Feat: detail statistik papan#1016
Conversation
|
🔄 AI PR Review sedang antri di server...
|
🔒 Security ReviewTotal Temuan: 4 isu (0 Critical, 2 High, 2 Medium)
|
| { | ||
| public function detailData(Request $request): View | ||
| { | ||
| $colomn = ''; |
There was a problem hiding this comment.
[HIGH] 🔒 Security: Input Validation Lemah - Potensi XSS via Filter Parameters
Masalah:
Controller menggunakan isset() untuk validasi input, kemudian langsung menggunakan htmlspecialchars() dan strip_tags() pada output. Namun, validasi input tidak memeriksa tipe data atau format yang diharapkan. Parameter filter['tipe'] dan filter['nilai'] bisa dimanipulasi untuk inject payload berbahaya yang mungkin lolos dari sanitasi sederhana.
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:
- Attacker bisa mengirim payload kompleks yang memanfaatkan edge case dari
strip_tags()danhtmlspecialchars() - Tidak ada whitelist validation untuk
filter['tipe']danfilter['nilai']- bisa berisi nilai arbitrary - Jika nilai ini digunakan di query atau logic lain (tidak terlihat di diff tapi mungkin di future), bisa menyebabkan injection
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'));
}| @@ -326,6 +326,11 @@ | |||
| Route::get('cetak', [App\Http\Controllers\DataPresisiPanganController::class, 'cetak'])->name('data-pokok.data-presisi-pangan.cetak'); | |||
There was a problem hiding this comment.
[HIGH] 🔒 Security: Authorization Bypass - Wrong Permission Middleware
Masalah:
Route baru untuk data presisi papan menggunakan permission middleware datapresisi-pangan-read padahal seharusnya datapresisi-papan-read. Ini adalah copy-paste error yang menyebabkan authorization logic salah.
Kode:
Route::prefix('data-presisi')->group(function () {
Route::prefix('papan')->group(function () {
Route::get('detail_data', [DataPresisiPapanController::class, 'detailData'])
->name('data-pokok.data-presisi-papan.detail_data');
})
->middleware(['permission:datapresisi-pangan-read']);
});Risiko:
- User dengan permission
datapresisi-pangan-readbisa mengakses data papan meskipun tidak punya permission untuk itu - Broken access control - user bisa melihat data yang seharusnya tidak boleh diakses
- Melanggar principle of least privilege
PoC (Chrome Console):
// Jalankan di Chrome DevTools Console (F12 → Console)
// Login sebagai user yang HANYA punya permission 'datapresisi-pangan-read'
// (tidak punya 'datapresisi-papan-read')
// Test akses ke endpoint papan
const testAccess = async () => {
const resp = await fetch('/data-presisi/papan/detail_data', {
method: 'GET',
headers: {
'Accept': 'text/html',
}
});
console.log('Status:', resp.status);
console.log('Access granted:', resp.status === 200);
if (resp.status === 200) {
console.log('⚠️ VULNERABILITY CONFIRMED: User dengan permission pangan bisa akses data papan!');
}
return resp;
};
testAccess();
// Expected: 403 Forbidden (jika permission benar)
// Actual: 200 OK (karena permission salah)Fix:
Route::prefix('data-presisi')->group(function () {
Route::prefix('papan')->group(function () {
Route::get('detail_data', [DataPresisiPapanController::class, 'detailData'])
->name('data-pokok.data-presisi-papan.detail_data');
})
->middleware(['permission:datapresisi-papan-read']); // Fix: ganti pangan → papan
});| orderable: false, | ||
| render: function(data, type, row, meta) { | ||
| return meta.row + meta.settings._iDisplayStart + 1; | ||
| } |
There was a problem hiding this comment.
[MEDIUM] 🔒 Security: Missing CSRF Token pada AJAX GET Request
Masalah:
DataTables AJAX request ke external API tidak menyertakan CSRF token. Meskipun ini GET request (yang secara default tidak memerlukan CSRF protection di Laravel), jika API endpoint berubah menjadi POST/PUT/DELETE di masa depan atau jika ada state-changing operation, ini bisa menjadi 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:
- Jika endpoint API berubah menjadi POST/PUT/DELETE, request akan gagal atau rentan CSRF
- Best practice: selalu sertakan CSRF token untuk consistency
- Jika ada side-effect di GET endpoint (bad practice tapi mungkin terjadi), bisa dieksploitasi
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() }}">| }; | ||
| var caption = { | ||
| title: customTitle || `Data Statistik ${categoryName}`, | ||
| period: '', |
There was a problem hiding this comment.
[MEDIUM] 🔒 Security: Missing CSRF Token pada AJAX GET Requests
Masalah:
Multiple AJAX requests ke external API tidak menyertakan CSRF token. Sama seperti issue di detail_data.blade.php, ini adalah defensive programming issue.
Kode:
// Di berbagai fungsi seperti loadChart, loadTable
$.ajax({
url: apiUrl + '/api/v1/statistik/papan',
method: 'GET',
headers: header, // hanya bearer token
data: {
tahun: selectedYear,
kategori: kategori
},
// ... rest
});Risiko:
- Sama seperti issue sebelumnya - jika endpoint berubah ke POST/PUT/DELETE
- Consistency issue - tidak ada standard CSRF protection
- Potential future vulnerability
PoC (Chrome Console):
// Jalankan di Chrome DevTools Console (F12 → Console)
// Test multiple endpoints tanpa CSRF token
const endpoints = [
'/api/v1/statistik/papan',
'/api/v1/statistik/papan/chart',
'/api/v1/statistik/papan/table'
];
const testCSRF = async () => {
for (const endpoint of endpoints) {
const resp = await fetch(`${apiUrl}${endpoint}?tahun=2024&kategori=status_kepemilikan`, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + bearerToken
// X-CSRF-TOKEN sengaja tidak disertakan
}
});
console.log(`Endpoint: ${endpoint}`);
console.log(`Status: ${resp.status}`);
console.log(`CSRF required: ${resp.status === 419 ? 'Yes' : 'No'}`);
console.log('---');
}
};
testCSRF();Fix:
// Tambahkan CSRF token di semua AJAX calls
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// Update header object
const header = {
'Authorization': 'Bearer ' + bearerToken,
'X-CSRF-TOKEN': csrfToken
};
// Gunakan di semua $.ajax() calls
$.ajax({
url: apiUrl + '/api/v1/statistik/papan',
method: 'GET',
headers: header, // sekarang include CSRF token
data: {
tahun: selectedYear,
kategori: kategori
},
// ... rest
});
// Pastikan meta tag CSRF ada di layout:
// <meta name="csrf-token" content="{{ csrf_token() }}">
⚡ Performance ReviewTotal Temuan: 0 isu (0 Critical, 0 High) ✅ Tidak ada isu performa CRITICAL atau HIGH yang terdeteksi pada kode yang ditambah/diubah. Analisis Hasil:Backend (PHP/Laravel):
Frontend:
Catatan:
|
📝 Code Quality ReviewTotal Temuan: 6 isu (0 Critical, 6 High)
|
| { | ||
| public function detailData(Request $request): View | ||
| { | ||
| $colomn = ''; |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Typo Variable Name
Kategori: PHP Quality
Masalah: Variable name typo $colomn seharusnya $column (terjadi di 3 tempat: line 12, 18, 21)
Kode: $colomn = $filter['column'] ?? null;
Fix:
$column = $filter['column'] ?? null;
// Dan update semua reference:
$column = htmlspecialchars(strip_tags($column), ENT_QUOTES, 'UTF-8');
'column' => $column,| use Illuminate\View\View; | ||
|
|
||
| class DataPresisiPapanController extends Controller | ||
| { |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Type Hints
Kategori: PHP Quality
Masalah: Parameter $filter tidak memiliki type hint. Untuk PHP 8+ wajib ada type hints untuk parameter dan return type.
Kode: public function detailData(Request $request, $filter = null)
Fix:
public function detailData(Request $request, ?array $filter = null): View
{
// ... existing code
}| class DataPresisiPapanController extends Controller | ||
| { | ||
| public function detailData(Request $request): View | ||
| { |
There was a problem hiding this comment.
[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:
// Buat FormRequest baru: app/Http/Requests/DataPresisiPapanRequest.php
class DataPresisiPapanRequest extends FormRequest
{
public function rules(): array
{
return [
'filter.column' => 'nullable|string|max:255',
'filter.nilai' => 'nullable|string|max:255',
'filter.tipe' => 'nullable|string|max:255',
'filter.judul' => 'nullable|string|max:255',
];
}
}
// Update controller:
public function detailData(DataPresisiPapanRequest $request, ?array $filter = null): View
{
$validated = $request->validated();
$filter = $validated['filter'] ?? null;
// ... rest of code
}| @@ -326,6 +326,11 @@ | |||
| Route::get('cetak', [App\Http\Controllers\DataPresisiPanganController::class, 'cetak'])->name('data-pokok.data-presisi-pangan.cetak'); | |||
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Wrong Permission Middleware
Kategori: Architecture
Masalah: Route untuk papan menggunakan permission datapresisi-pangan-read yang seharusnya untuk pangan, bukan papan. Ini kemungkinan copy-paste error.
Kode: ->middleware(['permission:datapresisi-pangan-read']);
Fix:
->middleware(['permission:datapresisi-papan-read']);| use Illuminate\View\View; | ||
|
|
||
| class DataPresisiPapanController extends Controller | ||
| { |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing Tests
Kategori: Testing
Masalah: Test coverage hanya mencakup happy path. Tidak ada test untuk XSS protection validation, edge cases (filter invalid, judul dengan karakter khusus), dan negative scenarios.
Kode: public function detailData(Request $request, $filter = null)
Fix:
// Tambahkan test cases di DataPresisiPapanControllerTest.php:
public function test_detail_data_sanitizes_xss_input()
{
$response = $this->actingAsAdmin()->get(route('data-pokok.data-presisi-papan.detail_data', [
'filter' => [
'column' => '<script>alert("xss")</script>',
'nilai' => 'test<script>',
'tipe' => 'kategori',
'judul' => 'Test<img src=x onerror=alert(1)>'
]
]));
$response->assertStatus(200);
$response->assertViewHas('column', '<script>alert("xss")</script>');
}
public function test_detail_data_handles_invalid_filter()
{
$response = $this->actingAsAdmin()->get(route('data-pokok.data-presisi-papan.detail_data', [
'filter' => 'invalid_string'
]));
$response->assertStatus(200);
}| use Illuminate\View\View; | ||
|
|
||
| class DataPresisiPapanController extends Controller | ||
| { |
There was a problem hiding this comment.
[HIGH] 📝 Code Quality: Missing PHPDoc
Kategori: PHP Quality
Masalah: Method tidak memiliki PHPDoc comment untuk dokumentasi parameter, return type, dan deskripsi fungsi.
Kode: public function detailData(Request $request, $filter = null)
Fix:
/**
* Display detail data presisi papan dengan filter opsional.
*
* @param Request $request HTTP request instance
* @param array|null $filter Filter data berisi column, nilai, tipe, dan judul
* @return View View detail data dengan DataTables
*/
public function detailData(Request $request, ?array $filter = null): View
{
// ... existing code
}
🐛 Bug Detection ReviewTotal Temuan: 1 isu (0 Critical, 1 High)
|
|
|
||
| if (isset($filter['tipe'], $filter['nilai']) && $filter['tipe'] && $filter['nilai']) { | ||
| $colomn = $filter['tipe'].':'.$filter['nilai']; | ||
| } |
There was a problem hiding this comment.
[HIGH] 🐛 Bug: Undefined Array Key Access
Kode: $judul = htmlspecialchars(strip_tags($request->judul)) . ' - ' . htmlspecialchars(strip_tags($request->filter[$colomn]));
Skenario: Ketika isset($request->judul) bernilai true tapi $request->filter[$colomn] tidak ada (misalnya user hanya mengirim parameter judul tanpa filter[tipe]), maka akan terjadi "Undefined array key" warning di PHP 8.0+ atau "Undefined index" notice di PHP 7.x. Ini bisa terjadi jika:
- Request dimanipulasi manual via URL/Postman
- Filter tipe dihapus dari request tapi judul tetap ada
- Race condition saat form submission
Dampak:
- PHP 8.0+: Warning "Undefined array key" muncul di log
- PHP 7.x: Notice "Undefined index"
- Halaman bisa crash atau menampilkan error 500 jika error reporting strict
- User experience buruk dengan error message yang tidak informatif
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;
}
🤖 AI Code Review — Selesai📋 Ringkasan Semua Review
Total inline comments: 11 |
|
Jika jumlah detail tidak sama, perlu cek dulu datanya simplescreenrecorder-2026-05-05_05.54.18.mp4 |
|
OK |
PR Description - Issue #1004
Depedency
Deskripsi Singkat
Feature ini mengimplementasikan halaman detail untuk Statistik Presisi Papan yang sebelumnya hanya menampilkan tabel statistik tanpa kemampuan melihat detail数据 penduduk per kategori/nilai. Pengguna sekarang dapat klik pada nilai di tabel statistik untuk melihat detail penduduk terkait.
Perubahan yang Dilakukan
1. File Baru:
app/Http/Controllers/DataPresisiPapanController.phpdetailData()menerima parameter:judul: Judul halaman detailfilter[tipe]: Tipe kategori (contoh: "Status Kepemilikan")filter[nilai]: Nilai yang dipilih (contoh: "Milik Sendiri")2. File Baru:
resources/views/data_pokok/data_presisi/papan/detail_data.blade.phpdata-presisi/papandari database gabungan3. Modifikasi:
app/Http/Controllers/StatistikPapanController.phpdetailLinkdariurl('')menjadiurl('data-presisi/papan/detail_data')4. Modifikasi:
resources/views/presisi/statistik/papan.blade.phptipeValuedanjudulUtamauntuk tracking kategori aktiffilter[nilai]dengan nilai database (attributes.nilai_db)filter[tipe]untuk informasi kategori5. Modifikasi:
routes/web.phppermission:datapresisi-pangan-read6. File Baru:
tests/Feature/DataPresisiPapanControllerTest.phpdetailData()dengan filterdetailData()tanpa filterAlasan Perubahan
Dampak Perubahan
Positif:
Yang Perlu Diperhatikan:
API_DATABASE_GABUNGAN_HOST)datapresisi-pangan-readdiperlukan untuk mengakses halamanTesting Checklist
php artisan test --filter DataPresisiPapanControllerTestRelated Issue
Catatan Tambahan
$colomnvariable untuk filtering API{databaseGabunganUrl}/api/v1/data-presisi/papanScreenshot
simplescreenrecorder-2026-04-27_10.25.35.mp4