Semua artikel
Insiden6 menit baca

7.403 aplikasi Lovable membocorkan data pengguna. Apakah tabel Supabase-mu salah satunya?

Translucent database cylinder with an open glowing drawer spilling envelopes and ID cards into the dark

Minggu ini perusahaan keamanan asal Hamburg, Crissy Field, bersama harian bisnis Jerman Handelsblatt, menerbitkan riset tentang aplikasi yang dibuat dengan Lovable. Mereka menarik 159.041 proyek Lovable dari arsip Common Crawl, mencakup aplikasi yang dibuat antara Desember 2025 dan September 2026, lalu mengajukan pertanyaan sederhana: apa yang diberikan database kepada orang asing yang sama sekali belum login?

Jawabannya tidak menggembirakan. Menurut tulisan Startup Fortune, 34.545 dari proyek itu memakai Supabase sebagai backend, dan 24.664 di antaranya mengembalikan record database tanpa autentikasi sama sekali. Di 7.403 situs, record itu memuat alamat email, tersebar di 10.560 tabel. RuntimeWire melaporkan bahwa data yang terekspos mencakup nomor paspor, informasi pembayaran, dan lebih dari 200.000 pesan chat. Satu penyedia layanan punya lebih dari 1.000 record paspor yang bisa dibaca; sebuah forum gaya hidup membocorkan 80.000 profil pengguna beserta pesan-pesan chat tersebut.

Tidak ada zero-day yang terlibat, dan tidak ada yang "diretas" dalam arti dramatis. Para peneliti memakai request read-only dengan key publik milik masing-masing situs. Datanya memang tersedia begitu saja bagi siapa pun yang memintanya.

Kalau kamu pernah merilis sesuatu di Supabase, terutama dengan AI builder, artikel ini memandumu mengecek aplikasimu sendiri dalam beberapa menit dan memperbaikinya dengan benar. Ini bukan hanya masalah Lovable: Bolt, Cursor, v0, dan aplikasi yang ditulis manual juga membuat kesalahan yang sama.

Apa yang sebenarnya salah

Supabase mengekspos tabel Postgres-mu lewat REST API yang dibuat otomatis. Frontend-mu berkomunikasi dengan API itu memakai anon key (disebut juga publishable key). Key itu memang dirancang untuk publik. Ia ikut terkirim di bundle JavaScript-mu, dan siapa pun bisa membacanya di DevTools.

Desain itu tidak masalah, asalkan setiap tabel mengaktifkan Row Level Security (RLS) dengan policy yang masuk akal. RLS-lah yang menentukan baris mana yang boleh dilihat oleh sebuah request. Seperti dicatat TheCyberSecGuru, mengutip dokumentasi Supabase sendiri: "Tanpa RLS, siapa pun yang memegang anon key-mu bisa mengakses semua data."

Jadi rantai kegagalannya seperti ini:

  • Sebuah tool AI (atau manusia yang kelelahan) membuat create table profiles (...).
  • Tidak ada yang menambahkan enable row level security.
  • Aplikasi berjalan sempurna saat diuji, karena bug-nya justru terletak pada sifatnya yang terlalu permisif.
  • Siapa pun yang menyalin anon key dari bundle-mu bisa menjalankan select * pada tabel itu.

Kalau kamu ingin memahami cara kerja RLS, panduan kami tentang Row Level Security Supabase membahas dasar-dasarnya. Di bawah ini kita fokus pada pengujian dan perbaikan.

Row Level Security Supabase: panduan lengkap

Cara mengecek aplikasimu (5 menit)

1. Lihat dari luar, seperti yang dilakukan para peneliti. Buka situs live-mu, buka DevTools, dan cari supabase.co di JavaScript yang dimuat. Kamu akan menemukan URL proyekmu dan anon key di sebelahnya. Lalu, dari terminal, minta satu baris dari tabel yang kamu anggap privat:

curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*&limit=1" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"

Arti responsnya:

  • Array kosong [] atau error izin: bagus, pengguna anonim tidak bisa membaca tabel itu.
  • Baris data sungguhan: kamu punya masalah yang sama dengan 7.403 situs tersebut.

Ulangi untuk setiap tabel yang menyimpan data pengguna: profiles, users, messages, orders, bookings, leads, waitlist, dan seterusnya. Tabel waitlist dan formulir kontak mudah terlupakan dan hampir selalu berisi email.

Selagi kamu memeriksa bundle, pastikan juga key yang kamu temukan adalah anon key, bukan key service_role. Service role sepenuhnya melewati RLS. Kalau key itu ada di frontend-mu, anggap sudah bocor dan rotasi sekarang juga: apa pun yang ada di bundle bisa dibaca oleh setiap pengunjung dan oleh setiap script pihak ketiga di halaman, seperti yang dialami pengguna Polymarket, dan bot sudah memburu key yang bocor. Artikel kami tentang merotasi secret aplikasimu tanpa downtime membahas proses rotasinya, dan API key yang terekspos di JavaScript sisi klien membahas apa lagi yang sering ikut masuk ke bundle.

API key di kode frontend: apa yang aman dirilis

2. Cek dari dalam. Di SQL editor Supabase, tampilkan semua tabel di schema public beserta status RLS-nya:

select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;

Setiap baris dengan rowsecurity = false bisa dibaca (dan mungkin ditulis) dengan anon key. Security Advisor di dashboard Supabase juga menandai tabel-tabel ini. Baca peringatannya, jangan diabaikan.

3. Cari kasus "RLS aktif, tapi tetap terbuka". Mengaktifkan RLS bukan garis finis. Tampilkan daftar policy-mu:

select tablename, policyname, roles, cmd, qual
from pg_policies
where schemaname = 'public';

Waspadai qual = true pada policy select yang berlaku untuk anon atau public. Itu policy yang menyatakan "semua orang boleh membaca semuanya", sama saja dengan tidak punya RLS. Tool AI kadang membuat persis policy ini supaya sebuah error hilang.

Cara memperbaikinya

Aktifkan RLS di setiap tabel:

alter table public.profiles enable row level security;
alter table public.messages enable row level security;
-- repeat for every table in public

Dengan RLS aktif dan tanpa policy, tabel terkunci: anon key tidak mendapat apa-apa. Itu default yang aman. Sekarang tambahkan hanya akses yang benar-benar kamu butuhkan.

Tulis policy yang sempit. Setup umum "pengguna hanya bisa melihat dan mengedit barisnya sendiri":

create policy "Read own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = user_id);

create policy "Update own profile"
on public.profiles for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

Hal-hal yang perlu diingat:

  • Batasi ke authenticated, bukan public, kecuali datanya memang benar-benar publik (artikel blog, daftar produk).
  • Pisahkan baca dan tulis. Sebuah tabel bisa aman dibaca publik tapi tetap butuh insert yang dikunci.
  • Tabel publik tetap membocorkan kolom. Kalau tabel profiles dibuat publik untuk menampilkan nama, kolom email atau nomor telepon di tabel yang sama juga ikut publik. Pindahkan field privat ke tabel terpisah atau ekspos view yang hanya berisi kolom yang aman.
  • View bisa melewati RLS. Di Postgres, view secara default berjalan dengan hak akses pemiliknya. Buat view dengan security_invoker = true agar RLS milik pemanggil yang berlaku.
  • Storage itu terpisah. Bucket publik menyajikan file kepada siapa pun yang punya URL-nya. KTP yang diunggah, invoice, dan avatar pengguna privat harus disimpan di bucket privat dengan storage policy.

Jadikan ini default, bukan pekerjaan bersih-bersih. Sampel para peneliti mencakup sepuluh bulan aplikasi baru, artinya masalah ini terus terulang di setiap tabel baru. Buat agar sulit dilupakan:

  • Tambahkan aturan ke instruksi proyek tool AI-mu: setiap create table di public harus diikuti enable row level security dan policy yang eksplisit.
  • Sebelum merge, periksa migration yang dihasilkan untuk create table yang tidak disertai enable row level security.
  • Jalankan ulang pengecekan dari luar setiap kali schema berubah, bukan hanya saat peluncuran.

Kalau kamu memakai Firebase dan bukan Supabase, kesalahan sejenis juga ada di sana dalam bentuk security rules yang terbuka. Lihat Firebase Security Rules: berhenti membiarkan database-mu terbuka.

Firebase Security Rules: kunci database-mu

Kalau kamu menemukan data yang terekspos

Memperbaiki policy menghentikan pembacaan di masa depan, tapi tidak membatalkan yang sudah terjadi. Jujurlah pada dirimu sendiri tentang apa yang bisa dijangkau dan selama berapa lama. Periksa log API Supabase untuk pembacaan massal yang tidak biasa dari IP yang tidak dikenal, rotasi semua secret yang tersimpan di tabel yang bisa dibaca, dan cari tahu kewajiban notifikasimu jika data pribadi seperti email, dokumen identitas, atau detail pembayaran terekspos. Di Uni Eropa, kemungkinan besar aturan pelanggaran data GDPR berlaku.

Checklist singkat

Sebelum menganggapnya selesai, pastikan bahwa:

  • Setiap tabel di public punya rowsecurity = true.
  • Tidak ada policy select yang memakai using (true) pada tabel berisi data privat.
  • Policy dibatasi ke authenticated dan terikat pada auth.uid().
  • Kolom privat tidak berada di tabel yang bisa dibaca publik.
  • View memakai security_invoker = true.
  • Hanya anon key yang ada di frontend-mu, tidak pernah service_role.
  • File privat disimpan di storage bucket privat.
  • Kamu menguji ulang dari luar setiap kali schema berubah.

Cek aplikasimu dari luar

Checklist keamanan untuk aplikasi buatan AI kami membahas hal-hal lain yang perlu dicek sebelum peluncuran. Bagian yang tidak nyaman dari riset ini adalah betapa biasa metodenya: key publik, request read-only, data kembali. Itulah persis sudut pandang penyerang, dan itulah sudut pandang yang seharusnya rutin kamu cek.

AppSafe memindai aplikasi live-mu dari luar. Scan penuh terverifikasi mencakup pengecekan read-only untuk database Supabase dan Firebase yang terbuka serta mencari API key yang terekspos di bundle JavaScript-mu, dan scan terjadwal bisa memberimu peringatan saat ada yang berubah setelah deploy. Penasaran seperti apa laporannya? Lihat contoh laporan.

Jalankan scan gratis untuk aplikasimu sekarang dan cari tahu apa yang bisa dilihat orang asing sebelum orang lain melakukannya.

Jalankan checklist ini secara otomatis

Satu pemindaian gratis memeriksa aplikasimu untuk semua masalah ini dan memberimu prompt perbaikan AI untuk tiap temuan.

Pindai aplikasiku gratis