Roster Publik Tanpa Bocor PII: Topeng di API, Bukan di Layar
Menyembunyikan kolom sensitif di UI bukan perlindungan. Topeng yang benar ada di lapisan API, dibuktikan asersi negatif E2E.
Ringkasan
Sering banget kolom sensitif disembunyiin di layar tapi API tetap kirim semua data, nah itu bocor bahkan ada namanya di OWASP. Solusinya masking di lapisan API pakai DTO yang cuma isinya data publik, sesuai UU PDP soal pengumpulan yang terbatas. Terus diuji pakai asersi negatif biar kebukti data pribadi nggak pernah nyampe ke browser.
Halaman daftar peserta program pembinaan pemuda akhirnya dibuka untuk publik. Persoalannya klasik: di dalam basis data, satu baris peserta tidak hanya berisi nama dan domisili. Ada tanggal lahir, nomor WhatsApp, sampai tautan dokumen pribadi. Pola lama yang sering saya temui adalah menyembunyikan kolom-kolom itu di antarmuka, sementara respons API tetap mengirim semuanya ke peramban. Terlihat aman di layar, padahal semua data sensitif sudah menyeberang jaringan.
Kegagalan model ini bahkan punya nama. OWASP mencatatnya sebagai API3:2023 Broken Object Property Level Authorization [1]: API cenderung mengembalikan semua properti objek, dan varian baca dari kelemahan ini dulu dikenal sebagai Excessive Data Exposure. Dampaknya langsung: bocornya data yang seharusnya tidak pernah keluar dari server.
Batas hukumnya bukan soal enak-enakan
Undang-Undang 27/2022 tentang Pelindungan Data Pribadi mengunci prinsip ini di Pasal 16 ayat (2) huruf a [5]: pengumpulan data pribadi dilakukan secara terbatas dan spesifik, sah secara hukum, dan transparan. Penjelasan resminya di JDIH Komdigi memperjelas makna "terbatas dan spesifik": pengumpulan dibatasi sesuai tujuan pemrosesan, dan tujuan itu sudah ditentukan secara eksplisit sejak saat pengumpulan [3]. Daftar publik hanya butuh nama, bidang program, lokasi kegiatan, dan domisili. Kalau data yang dikumpulkan jauh lebih banyak dari yang bisa dipublikasikan, posisinya memang salah sejak awal, bukan salahnya halaman publiknya.
Satu koreksi yang sering keliru: prinsip ini tidak ada di Pasal 8. Pasal 8 isinya hak subjek untuk mengakhiri pemrosesan dan meminta penghapusan data. Saya sempat mengira kaitannya di sana, dan baru yakin setelah membaca teks lengkapnya. Detail kecil seperti ini penting kalau nanti ada yang memverifikasi klaim di artikel ke pasal yang disebut.
Topeng dipasang di API, bukan di layar
Keputusan utama pada perbaikan ini: pemotongan field sensitif terjadi di lapisan API. Endpoint daftar publik hanya mengonstruksi DTO proyeksi yang berisi nama, bidang kepeloporan, lokasi kegiatan, dan domisili. Tanggal lahir, nomor WhatsApp, dan tautan dokumen sama sekali tidak keluar dari server. Halaman publik tinggal merender DTO itu apa adanya. Tidak ada logika "jangan tampilkan ini" di sisi klien, karena tidak ada yang perlu disembunyikan.
{
"full_record": {
"id": "usr_88392",
"nama": "Budi Santoso",
"tanggal_lahir": "1995-08-12",
"whatsapp": "081234567890",
"domisili": "Kota Nusantara",
"bidang_program": "Pelatihan Digital",
"lokasi_kegiatan": "Gedung Pemuda Lt. 2",
"tautan_dokumen": "https://drive.example.com/file/xyz123"
},
"masked_dto": {
"nama": "Budi Santoso",
"bidang_program": "Pelatihan Digital",
"lokasi_kegiatan": "Gedung Pemuda Lt. 2",
"domisili": "Kota Nusantara"
}
}Tabel admin tetap butuh data lengkap untuk operasional yang sah, jadi jalurnya dipisah: hanya-baca, terkunci izin baca modul, dan terbatas pada lingkup bidang kerja operator. Bukan akses penuh yang diberikan lalu diharapkan tidak disalahgunakan.
Janjinya diuji mesin, bukan diuji niat
Masking di API tanpa pengujian hanya tinggal janji di code review. Karena itu spec E2E-nya menegakkan sisi negatifnya: bukan mengecek nama tampil, tapi memastikan pola terlarang tidak pernah muncul di DOM halaman yang dirender. Pola nomor ponsel Indonesia, tanggal berformat ISO, dan domain penyimpanan dokumen dilarang keras. Menariknya, asersi expect di Playwright [2] otomatis mencoba ulang sampai kondisi terpenuhi atau timeout, jadi halaman yang render-nya asinkron tidak membuat pembacaan body jadi flaky.
import { test, expect } from "@playwright/test";
test("public roster does not leak PII", async ({ page }) => {
await page.goto("/program/public-roster");
const body = await page.locator("body").innerText();
// No Indonesian mobile number pattern
expect(body).not.toMatch(/08\d{8,11}/);
// No ISO-formatted date (YYYY-MM-DD)
expect(body).not.toMatch(/\d{4}-\d{2}-\d{2}/);
// No private document-storage link
expect(body).not.toContain("drive.example.com");
});Asersi negatif seperti ini menangkap regresi dari lapisan mana pun. Kapanpun seseorang menambah field sensitif ke DTO, memanggil endpoint lain yang belum termasking, atau merender data mentah di komponen baru, pengujian ini berhenti merah sebelum kode menyentuh produksi. Jaminan keamanan berubah dari "semoga tidak ada yang lupa" menjadi fakta yang bisa diverifikasi ulang kapanpun di pipeline.
Satu lapisan lagi di bawah: view basis data
Kalau ingin menegakkan proyeksi lebih awal, basis data bisa jadi titik kendali. CREATE VIEW di PostgreSQL [4] menghasilkan tampilan yang tidak dimaterialisasi secara fisik; kuerinya dijalankan setiap kali view direferensikan. View yang hanya memilih kolom publik tidak akan pernah basi seperti salinan cache, karena datanya selalu dihitung ulang dari tabel dasar. Untuk kebutuhan penyaringan tingkat baris, PostgreSQL menyediakan opsi security_barrier agar aturan filter dievaluasi lebih dulu. DTO di API tetap menjadi kontrak publik, tapi view membuat kolom sensitif semakin sulit terbawa tanpa sengaja.
Pola ini tidak spesifik untuk program pemuda. Roster dosen, daftar relawan, direktori anggota: semua menghadapi soal yang sama. Tempatkan topeng di lapisan API, buat halaman publik hanya mengenal DTO, lalu pasang asersi negatif yang membuktikan data pribadi tidak pernah sampai ke DOM. Hukumnya sudah mengatakan pengumpulan harus terbatas pada tujuan; kode dan pengujian tinggal memastikan janji itu ditepati di setiap respons.
Sources
- [1] API3:2023 Broken Object Property Level Authorization — OWASP API Security Top 10
- [2] Assertions — Playwright Documentation
- [3] Penjelasan resmi UU 27/2022 — JDIH Kementerian Komunikasi dan Digital
- [4] CREATE VIEW — PostgreSQL 17 Documentation
- [5] Undang-Undang Nomor 27 Tahun 2022 tentang Pelindungan Data Pribadi (teks lengkap)