Skip to content

[P0][Security] Perbaiki Kebocoran PII dan Pengamanan Endpoint Datatables Pengguna OpenSID - #714

Merged
pandigresik merged 2 commits into
rilis-devfrom
dev-51
Sep 1, 2026
Merged

[P0][Security] Perbaiki Kebocoran PII dan Pengamanan Endpoint Datatables Pengguna OpenSID#714
pandigresik merged 2 commits into
rilis-devfrom
dev-51

Conversation

@habibie11

Copy link
Copy Markdown
Contributor

issue # https://github.com/OpenSID/wiki-keamanan/issues/51

🎯 Deskripsi

Pull request ini menangani masalah keamanan kritis (P0 / Critical Security Issue) terkait kebocoran data pribadi (PII) dan informasi infrastruktur desa pada endpoint publik GET /datatables/pengguna-opensid dan GET /datatables/semua-desa.

Sebelumnya, endpoint tersebut dapat diakses secara terbuka tanpa login hanya dengan header X-Requested-With: XMLHttpRequest dan menjalankan query SELECT * via Desa::semuaDesa(). Hal ini menyebabkan bocornya data sensitif seperti email_desa, telepon, nomor HP/nama/jabatan operator pada kontak, IP internal/hosting (ip_lokal, ip_hosting), alamat kantor, koordinat presisi (lat, lng), dan URL internal/hosting.

Melalui PR ini, diterapkan pendekatan Option C (Conditional Data per Auth Status) yang tetap menjaga fungsi tampilan halaman web publik (/web/opensid-data dan modal di /web/opensid) namun dengan pembatasan ketat:

  1. Whitelist Kolom Publik: Pengunjung tanpa login hanya mendapatkan data agregat/tampilan umum (nama_desa, wilayah, versi, status TTE, updated_at) tanpa data PII maupun infrastruktur.
  2. Whitelist Kolom Terautentikasi: User terautentikasi (login) dapat melihat data yang lebih lengkap (seperti url_hosting dan kode_desa) untuk verifikasi, namun data kontak pribadi operator tetap tidak diekspos sembarangan.
  3. Pembatasan Dump Massal (Length Limiting): Membatasi parameter length DataTables maksimal 25 baris untuk publik dan 100 baris untuk user login.
  4. Rate Limiting: Menambahkan middleware throttle:60,1 pada seluruh endpoint DataTables publik guna mencegah scraping massal otomatis.
  5. Proteksi Export Excel: Menolak permintaan ekspor data Excel (?excel=1) dengan 403 Forbidden jika pengguna belum login.
  6. Proteksi Route Admin: Memindahkan endpoint kabupaten-kosong ke grup middleware auth.

🛠️ Perubahan yang Dilakukan

1. app/Models/Desa.php

Security Fix — Penambahan scope whitelist kolom aman:

  • Menambahkan scopeSemuaDesaPublik($query): Memilih hanya kolom-kolom non-sensitif untuk konsumsi publik. Kolom PII (email_desa, telepon, kontak, ip_lokal, ip_hosting, lat, lng, alamat_kantor, dll) secara eksplisit tidak disertakan.
  • Menambahkan scopeSemuaDesaAuth($query): Memilih kolom untuk pengguna terautentikasi dengan menyertakan url_hosting dan kode_desa.
+    /**
+     * Scope semua desa untuk tampilan publik — whitelist kolom aman, tanpa PII.
+     */
+    public function scopeSemuaDesaPublik($query)
+    {
+        return $query
+            ->select([
+                'nama_desa',
+                'nama_kecamatan',
+                'kode_kecamatan',
+                'nama_kabupaten',
+                'kode_kabupaten',
+                'nama_provinsi',
+                'kode_provinsi',
+                'versi_lokal',
+                'versi_hosting',
+                'modul_tte',
+                'jml_surat_tte',
+                'updated_at',
+                'created_at',
+            ])
+            ->selectRaw('(CASE WHEN (versi_hosting IS NULL) THEN versi_lokal WHEN (versi_lokal IS NULL) THEN versi_hosting WHEN (tgl_rekam_hosting > tgl_rekam_lokal) THEN versi_hosting ELSE versi_lokal END) as versi')
+            ->when(session('provinsi'), function ($query, $provinsi) {
+                $query->where('kode_provinsi', $provinsi->kode_prov);
+            });
+    }
+
+    /**
+     * Scope semua desa untuk user terautentikasi — kolom lebih lengkap termasuk URL.
+     */
+    public function scopeSemuaDesaAuth($query)
+    {
+        return $query
+            ->select([
+                'nama_desa',
+                'kode_desa',
+                'nama_kecamatan',
+                'kode_kecamatan',
+                'nama_kabupaten',
+                'kode_kabupaten',
+                'nama_provinsi',
+                'kode_provinsi',
+                'versi_lokal',
+                'versi_hosting',
+                'modul_tte',
+                'jml_surat_tte',
+                'updated_at',
+                'created_at',
+                'url_hosting',
+            ])
+            ->selectRaw('(CASE WHEN (versi_hosting IS NULL) THEN versi_lokal WHEN (versi_lokal IS NULL) THEN versi_hosting WHEN (tgl_rekam_hosting > tgl_rekam_lokal) THEN versi_hosting ELSE versi_lokal END) as versi')
+            ->when(session('provinsi'), function ($query, $provinsi) {
+                $query->where('kode_provinsi', $provinsi->kode_prov);
+            });
+    }

2. app/Http/Controllers/DashboardController.php

Security Fix — Implementasi conditional scope, length limiter, dan guard export:

  • Pada datatablePenggunaOpensid:
    • Menetapkan batas maksimum length menjadi 25 baris untuk publik dan 100 baris untuk user login.
    • Mengarahkan query ke semuaDesaAuth() jika login dan semuaDesaPublik() jika tidak login.
  • Pada datatableSemuaDesa:
    • Mengarahkan query ke semuaDesaAuth() jika login dan semuaDesaPublik() jika tidak login.
    • Menambahkan validasi if (! auth()->check()) abort(403, 'Unauthorized'); saat ada parameter request excel=1.
     public function datatableSemuaDesa(Request $request)
     {
         if ($request->excel) {
+            // Export Excel hanya untuk user yang sudah login
+            if (! auth()->check()) {
+                abort(403, 'Unauthorized');
+            }
             $paramDatatable = json_decode($request->get('params'), 1);
             $request->merge($paramDatatable);
         }
@@ -80,7 +84,12 @@
         ];
 
         if ($request->ajax() || $request->excel) {
-            $query = DataTables::of($this->desa->fillter($filters)->semuaDesa());
+            // Pilih scope berdasarkan status autentikasi
+            // Auth: kolom lebih lengkap (+url_hosting), Publik: hanya kolom aman tanpa PII
+            $queryBuilder = auth()->check()
+                ? $this->desa->fillter($filters)->semuaDesaAuth()
+                : $this->desa->fillter($filters)->semuaDesaPublik();
+
+            $query = DataTables::of($queryBuilder);
     public function datatablePenggunaOpensid(Request $request)
     {
+        // Batasi jumlah baris per request untuk mencegah dump massal data
+        // Auth: max 100 baris, Publik: max 25 baris
+        $maxLength = auth()->check() ? 100 : 25;
+        if ($request->has('length') && (int) $request->length > $maxLength) {
+            $request->merge(['length' => $maxLength]);
+        }
+
         $filters = [
@@ -420,7 +424,10 @@
         ];
 
         if ($request->ajax()) {
-            return DataTables::of($this->desa->fillter($filters)->semuaDesa())
+            // Pilih scope berdasarkan status autentikasi
+            // Auth: kolom lebih lengkap (+url_hosting), Publik: hanya kolom aman tanpa PII
+            $query = auth()->check()
+                ? $this->desa->fillter($filters)->semuaDesaAuth()
+                : $this->desa->fillter($filters)->semuaDesaPublik();
+
+            return DataTables::of($query)

3. routes/web.php

Security Fix — Penambahan middleware throttle dan pemisahan grup route:

  • Menambahkan middleware throttle:60,1 pada grup datatables publik.
  • Memindahkan endpoint datatables/kabupaten-kosong ke grup baru dengan middleware ['auth', 'throttle:60,1'].
-// datatable
-Route::prefix('datatables')->as('datatables:')
+// datatable — publik dengan rate limiting
+// Data yang dikembalikan dikontrol di controller berdasarkan auth status (Option C Issue #51).
+Route::prefix('datatables')->as('datatables:')
+    ->middleware('throttle:60,1')
     ->group(function () {
         Route::get('desa-baru', [DashboardController::class, 'datatableDesaBaru'])->name('desa-baru');
         Route::get('semua-desa', [DashboardController::class, 'datatableSemuaDesa'])->name('semua-desa');
-        Route::get('kabupaten-kosong', [DashboardController::class, 'datatableKabupatenKosong'])->name('kabupaten-kosong');
         Route::get('opendk-baru', [DashboardController::class, 'datatableOpendkBaru'])->name('opendk-baru');
...
+    });
+
+// datatable — khusus admin, tidak dipakai di halaman publik manapun
+Route::prefix('datatables')->as('datatables:')
+    ->middleware(['auth', 'throttle:60,1'])
+    ->group(function () {
+        Route::get('kabupaten-kosong', [DashboardController::class, 'datatableKabupatenKosong'])->name('kabupaten-kosong');
     });

✅ Test Cases yang Diimplementasikan

  • Request publik ke /datatables/pengguna-opensid tanpa login tidak lagi mengembalikan data PII (email_desa, telepon, kontak, ip_lokal, ip_hosting, lat, lng, alamat_kantor).
  • Request publik ke /datatables/semua-desa tanpa login tidak lagi mengembalikan data PII maupun infrastruktur.
  • Parameter length yang besar (misal length=9999 atau length=1000) pada request publik otomatis dibatasi menjadi maksimal 25 baris data.
  • Percobaan download Excel (/datatables/semua-desa?excel=1) tanpa login mengembalikan respon 403 Forbidden.
  • Akses ke /datatables/kabupaten-kosong tanpa login di-redirect (302) ke halaman login.
  • Halaman web publik /web/opensid-data dan modal pengguna di /web/opensid tetap dapat menampilkan data agregat tanpa error.
  • Seluruh unit tests bawaan aplikasi lulus (PASSED).

📸 Cara Menjalankan Uji Coba Manual

  1. Uji Coba Respon Publik (Tanpa Login):
    Jalankan perintah cURL berikut di terminal:

    curl -s -H "X-Requested-With: XMLHttpRequest" \
      "http://localhost:8000/datatables/pengguna-opensid?draw=1&start=0&length=2"
    • Verifikasi: Periksa objek JSON yang dihasilkan pada array data. Pastikan kolom seperti email_desa, telepon, kontak, ip_lokal, ip_hosting, lat, lng, dan alamat_kantor tidak ada di dalam respon.
  2. Uji Coba Pembatasan Length (Anti-Dump):

    curl -s -H "X-Requested-With: XMLHttpRequest" \
      "http://localhost:8000/datatables/pengguna-opensid?draw=1&start=0&length=500"
    • Verifikasi: Jumlah item dalam array data yang dikembalikan maksimal 25 baris data.
  3. Uji Coba Proteksi Export Excel Tanpa Login:

    curl -s -o /dev/null -w "%{http_code}" \
      "http://localhost:8000/datatables/semua-desa?excel=1&params=%7B%7D"
    • Verifikasi: Mengembalikan HTTP status code 403.
  4. Uji Coba Proteksi Endpoint Admin (Kabupaten Kosong):

    curl -s -o /dev/null -w "%{http_code}" \
      -H "X-Requested-With: XMLHttpRequest" \
      "http://localhost:8000/datatables/kabupaten-kosong?draw=1"
    • Verifikasi: Mengembalikan HTTP status code 302 (redirect ke login) atau 401.
  5. Uji Coba UI Frontend:

    • Buka halaman publik /web/opensid-data di browser. Tabel tetap terisi dan filter wilayah berfungsi dengan baik.
    • Buka halaman /web/opensid, buka modal tabel selengkapnya. Data tetap tampil dengan normal tanpa mengekspos PII.

🤖 Cara Menjalankan Uji Coba Otomatis (Automated Test)

Jalankan test suite untuk memastikan tidak ada regresi pada sistem:

php artisan test --testsuite=Unit

📸 Screenshot atau Video

[Isi dengan tangkapan layar respon JSON yang telah dibersihkan dari PII atau rekaman browser]


⚠️ Catatan Penting

  • Pengunjung publik yang tidak login tetap dapat menggunakan tabel DataTables pada halaman /web/opensid-data dan /web/opensid secara normal.
  • Data yang disembunyikan untuk publik mencakup data PII individu desa (email, telepon, kontak operator) serta data infrastruktur (IP hosting/lokal, koordinat presisi).
  • Rate limiter throttle:60,1 membatasi maksimal 60 request per menit per IP untuk mencegah scraping massal otomatis.

@habibie11
habibie11 requested a review from vickyrolanda August 31, 2026 04:51
@github-actions

Copy link
Copy Markdown

🔄 AI PR Review sedang antri di server...

Proses review akan segera dimulai di background — hasil akan muncul sebagai komentar setelah selesai.
Powered by CrewAI · PR #714

@pandigresik
pandigresik merged commit 182a7f8 into rilis-dev Sep 1, 2026
1 check failed
@pandigresik
pandigresik deleted the dev-51 branch September 1, 2026 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants