-
Notifications
You must be signed in to change notification settings - Fork 13
Feat: detail statistik papan #1016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dd6d61a
25eb626
cefa424
bc0a1a1
eed1598
0c36d89
14b5d1e
6cbe27f
087e4ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,3 +26,4 @@ package-lock.json | |
| /playwright-report/ | ||
| .env.e2e | ||
| /template_ai/ | ||
| AGENTS.md | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| <?php | ||
|
|
||
| namespace App\Http\Controllers; | ||
|
|
||
| use Illuminate\Http\Request; | ||
| use Illuminate\View\View; | ||
|
|
||
| class DataPresisiPapanController extends Controller | ||
| { | ||
| /** | ||
| * Display detail data presisi papan dengan filter opsional. | ||
| * | ||
| * @param Request $request HTTP request instance | ||
| * @return View View detail data dengan DataTables | ||
| */ | ||
| public function detailData(Request $request): View | ||
| { | ||
| $colomn = ''; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] 🔒 Security: Input Validation Lemah - Potensi XSS via Filter Parameters Masalah: Kode: if (isset($filter['tipe'])) {
$colomn = $filter['tipe'];
$judul = htmlspecialchars(strip_tags($filter['judul'] ?? ''), ENT_QUOTES, 'UTF-8');
}
if (isset($filter['nilai'])) {
$colomn = $filter['nilai'];
$judul = htmlspecialchars(strip_tags($filter['judul'] ?? ''), ENT_QUOTES, 'UTF-8');
}Risiko:
PoC (Chrome Console): // Jalankan di Chrome DevTools Console (F12 → Console)
// Pastikan sudah login ke aplikasi di tab yang sama
// Test 1: XSS payload via filter parameter
const xssPayload = {
tipe: '<script>alert("XSS")</script>',
nilai: '"><img src=x onerror=alert("XSS2")>',
judul: '<svg/onload=alert("XSS3")>'
};
const url = new URL('/data-presisi/papan/detail_data', window.location.origin);
Object.keys(xssPayload).forEach(key => {
url.searchParams.append(`filter[${key}]`, xssPayload[key]);
});
// Navigate ke URL dengan payload
window.location.href = url.toString();
// Test 2: Cek apakah payload muncul di page source
// Setelah page load, buka DevTools dan cek:
console.log('Check page source for unescaped payload');
console.log(document.documentElement.innerHTML.includes('<script>'));Fix: use App\Http\Requests\DataPresisiPapanRequest;
// Buat FormRequest baru: app/Http/Requests/DataPresisiPapanRequest.php
class DataPresisiPapanRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'filter.tipe' => 'nullable|string|in:status_kepemilikan,jenis_lantai,jenis_dinding,kondisi_dinding,jenis_atap,kondisi_atap,sumber_penerangan,daya_listrik,energi_memasak,fasilitas_bab,sumber_air_minum,cara_memperoleh_air,kualitas_air',
'filter.nilai' => 'nullable|string|max:255',
'filter.judul' => 'nullable|string|max:255',
];
}
}
// Update controller method signature:
public function detailData(DataPresisiPapanRequest $request)
{
$filter = $request->validated()['filter'] ?? [];
$column = null;
$judul = '';
if (isset($filter['tipe'])) {
$column = $filter['tipe'];
$judul = $filter['judul'] ?? '';
}
if (isset($filter['nilai'])) {
$column = $filter['nilai'];
$judul = $filter['judul'] ?? '';
}
return view('data_pokok.data_presisi.papan.detail_data', compact('column', 'judul'));
}There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] 📝 Code Quality: Typo Variable Name Kategori: PHP Quality Fix: $column = $filter['column'] ?? null;
// Dan update semua reference:
$column = htmlspecialchars(strip_tags($column), ENT_QUOTES, 'UTF-8');
'column' => $column, |
||
| $title = 'Data Presisi Papan - ' . $request->input('judul'); | ||
| $title = htmlspecialchars(strip_tags($title), ENT_QUOTES, 'UTF-8'); | ||
| $filter = $request->input('filter'); | ||
|
|
||
| if (isset($filter['tipe'], $filter['nilai']) && $filter['tipe'] && $filter['nilai']) { | ||
| $colomn = $filter['tipe'] . ':' . $filter['nilai']; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] 🐛 Bug: Undefined Array Key Access Kode: Skenario: Ketika
Dampak:
Fix: $judul = 'Detail Data Presisi Papan';
if (isset($request->judul)) {
$colomn = $request->filter['tipe'];
// Tambahkan validasi key exists sebelum akses
$nilaiFilter = isset($request->filter[$colomn]) ? htmlspecialchars(strip_tags($request->filter[$colomn])) : 'Tidak Diketahui';
$judul = htmlspecialchars(strip_tags($request->judul)) . ' - ' . $nilaiFilter;
}Atau lebih baik dengan null coalescing: $judul = 'Detail Data Presisi Papan';
if (isset($request->judul) && isset($request->filter['tipe'])) {
$colomn = $request->filter['tipe'];
$nilaiFilter = htmlspecialchars(strip_tags($request->filter[$colomn] ?? 'Tidak Diketahui'));
$judul = htmlspecialchars(strip_tags($request->judul)) . ' - ' . $nilaiFilter;
} |
||
|
|
||
| return view('data_pokok.data_presisi.papan.detail_data', compact('title', 'colomn')); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| @extends('layouts.index') | ||
|
|
||
| @section('title', $title) | ||
|
|
||
| @section('content_header') | ||
| <h1>{{ html_entity_decode($title) }}</h1> | ||
| @stop | ||
|
|
||
| @section('content') | ||
| @include('partials.breadcrumbs') | ||
|
|
||
| <div class="row"> | ||
| <div class="col-lg-12"> | ||
| <div class="card card-outline card-primary"> | ||
| <div class="card-header"> | ||
| <div class="row"> | ||
| <x-filter-tahun :selectedYear="request('tahun')" /> | ||
| </div> | ||
| </div> | ||
| <div class="card-body"> | ||
| <div class="table-responsive"> | ||
| <table class="table table-striped" id="detail-papan"> | ||
| <thead> | ||
| <tr> | ||
| <th>NO</th> | ||
| <th>NIK</th> | ||
| <th>NOMOR KK</th> | ||
| <th>NAMA</th> | ||
| <th>STATUS KEPEMILIKAN</th> | ||
| <th>LUAS LANTAI (M²)</th> | ||
| <th>JENIS LANTAI</th> | ||
| <th>JENIS DINDING</th> | ||
| <th>SUMBER AIR MINUM</th> | ||
| <th>SUMBER PENERANGAN</th> | ||
| <th>DAYA TERPASANG</th> | ||
| <th>DAYA TERPASANG 2</th> | ||
| <th>DAYA TERPASANG 3</th> | ||
| <th>TANGGAL PENGISIAN</th> | ||
| <th>STATUS PENGISIAN</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody></tbody> | ||
| </table> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| @endsection | ||
|
|
||
| @section('js') | ||
| <script nonce="{{ csp_nonce() }}"> | ||
| document.addEventListener("DOMContentLoaded", function(event) { | ||
| const headers = @include('layouts.components.header_bearer_api_gabungan'); | ||
| var url = new URL("{{ config('app.databaseGabunganUrl').'/api/v1/data-presisi/papan' }}"); | ||
| var papan = $('#detail-papan').DataTable({ | ||
| processing: true, | ||
| serverSide: true, | ||
| autoWidth: false, | ||
| ordering: true, | ||
| searchPanes: { | ||
| viewTotal: false, | ||
| columns: [0] | ||
| }, | ||
| ajax: { | ||
| url: url.href, | ||
| headers: headers, | ||
| method: 'get', | ||
| data: function(row) { | ||
| const data = { | ||
| "page[size]": row.length, | ||
| "page[number]": (row.start / row.length) + 1, | ||
| "filter[search]": row.search.value, | ||
| "filter[tahun]": $('#filter-tahun').val(), | ||
| "filter[colomn]": '{{ $colomn }}', | ||
| "sort": (row.order[0]?.dir === "asc" ? "" : "-") + row.columns[row.order[0]?.column]?.name, | ||
| }; | ||
|
|
||
| return data; | ||
| }, | ||
| dataSrc: function(json) { | ||
| json.recordsTotal = json.meta.pagination.total; | ||
| json.recordsFiltered = json.meta.pagination.total; | ||
| return json.data; | ||
| }, | ||
| }, | ||
| columnDefs: [{ | ||
| targets: '_all', | ||
| className: 'text-nowrap', | ||
| }, { | ||
| targets: [0, 1, 2, 3, 4, 5], | ||
| orderable: false, | ||
| searchable: false, | ||
| }], | ||
| columns: [{ | ||
| data: null, | ||
| orderable: false, | ||
| render: function(data, type, row, meta) { | ||
| return meta.row + meta.settings._iDisplayStart + 1; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MEDIUM] 🔒 Security: Missing CSRF Token pada AJAX GET Request Masalah: Kode: ajax: {
url: `${apiUrl}/api/v1/data-presisi/papan`,
type: 'GET',
headers: header, // hanya bearer token, tidak ada X-CSRF-TOKEN
data: function(d) {
d.tahun = selectedYear;
if (column) {
d[`filter[${column}]`] = judul;
}
},
// ... rest of config
}Risiko:
PoC (Chrome Console): // Jalankan di Chrome DevTools Console (F12 → Console)
// Simulasi CSRF attack jika endpoint berubah ke POST
// Buat form tersembunyi di attacker site
const attackForm = document.createElement('form');
attackForm.method = 'POST';
attackForm.action = 'https://target-site.com/api/v1/data-presisi/papan';
attackForm.style.display = 'none';
// Tambah field
const tahunField = document.createElement('input');
tahunField.name = 'tahun';
tahunField.value = '2024';
attackForm.appendChild(tahunField);
// Submit form (akan gagal karena CORS, tapi konsep CSRF tetap valid)
document.body.appendChild(attackForm);
attackForm.submit();
console.log('⚠️ CSRF attack simulated - jika endpoint POST tanpa CSRF protection, ini akan berhasil');Fix: // Tambahkan CSRF token di header
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
ajax: {
url: `${apiUrl}/api/v1/data-presisi/papan`,
type: 'GET',
headers: {
...header,
'X-CSRF-TOKEN': csrfToken // Tambahkan CSRF token
},
data: function(d) {
d.tahun = selectedYear;
if (column) {
d[`filter[${column}]`] = judul;
}
},
// ... rest of config
}
// Pastikan meta tag CSRF ada di layout:
// <meta name="csrf-token" content="{{ csrf_token() }}"> |
||
| }, | ||
| { | ||
| data: 'attributes.nik', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: 'attributes.no_kk', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: 'attributes.nama', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: "attributes.kd_stat_bangunan_tinggal", | ||
| defaultContent: 'N/A', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: "attributes.luas_lantai", | ||
| defaultContent: 'N/A', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: "attributes.kd_jenis_lantai_terluas", | ||
| defaultContent: 'N/A', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: "attributes.kd_jenis_dinding", | ||
| defaultContent: 'N/A', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: "attributes.kd_sumber_air_minum", | ||
| defaultContent: 'N/A', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: "attributes.kd_sumber_penerangan_utama", | ||
| defaultContent: 'N/A', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: "attributes.kd_daya_terpasang", | ||
| defaultContent: 'N/A', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: "attributes.kd_daya_terpasang2", | ||
| defaultContent: 'N/A', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: "attributes.kd_daya_terpasang3", | ||
| defaultContent: 'N/A', | ||
| orderable: false, | ||
| }, | ||
| { | ||
| data: 'attributes.tanggal_pengisian', | ||
| orderable: false | ||
| }, | ||
| { | ||
| data: 'attributes.status_pengisian', | ||
| orderable: false | ||
| }, | ||
| ], | ||
| }); | ||
| papan.on('draw.dt', function() { | ||
| var PageInfo = $('#detail-papan').DataTable().page.info(); | ||
| papan.column(0, { | ||
| page: 'current' | ||
| }).nodes().each(function(cell, i) { | ||
| cell.innerHTML = i + 1 + PageInfo.start; | ||
| }); | ||
| }); | ||
|
|
||
| // Event listener for year filter change | ||
| $('#filter-tahun').on('change', function() { | ||
| papan.ajax.reload(); | ||
| }); | ||
| }); | ||
| </script> | ||
| @endsection | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[HIGH] 📝 Code Quality: Manual Input Validation
Kategori: Architecture
Masalah: Validasi input menggunakan
isset()manual. Best practice Laravel adalah menggunakan FormRequest untuk validasi yang lebih robust dan reusable.Kode:
if (isset($filter['column']) && isset($filter['nilai']) && isset($filter['tipe']) && isset($filter['judul'])) {Fix: