Yazılar · Supabase · PostgreSQL · Güvenlik
Supabase'de yedek tablo almak yayımlamak demektir: RLS'in üç sessiz varsayılanı
Supabase'de create table … as select ile açılan tabloda RLS kapalı doğar, public şeması PostgREST tarafından otomatik yayımlanır ve varsayılan grant'lar anon rolüne SELECT verir. Üçü birleşince tablo açmak yayımlamak olur; sitenin kaynak kodundaki anon anahtarla herkes okur. Yedeği public dışında bir şemaya al ya da açar açmaz RLS'i etkinleştirip anon ve authenticated rollerinden bütün yetkileri geri al.
Bir sabah, bir sitenin veritabanında hukuki gerekçeyle kişisel veri temizliği yaptım: yüzlerce kayıttan yakınların adları ve doğum tarihleri silindi. Temizlik öncesi, her aklı başında insanın yapacağı gibi, yedek aldım. Aynı gün akşam fark ettim ki o yedek tablo internetten okunabiliyor. Temizliğin tamamı geçersizdi; sildiğim her şey yanındaki tablodan servis ediliyordu. Saldırıya gerek yoktu, anahtar zaten sitenin JavaScript'inin içindeydi.
Olay, kanıtıyla
create table public.kayitlar_yedek_20260812 as
select id, ozet, biyografi from public.kayitlar;
Ve birkaç saat sonra, sitenin kendi anon anahtarıyla:
curl -H "apikey: sb_publishable_…" \
"https://<proje>.supabase.co/rest/v1/kayitlar_yedek_20260812?select=*"
# HTTP 200 — 592 kayıt, 674 KB
Üç varsayılan üst üste biniyor
Tek başına hiçbiri hata değil; üçü aynı anda geçerli olunca tablo açmak yayımlamak oluyor. Birincisi, create table … as select ile açılan tabloda satır düzeyi güvenlik varsayılan olarak kapalıdır. İkincisi, public şeması PostgREST tarafından otomatik yayımlanır; tablo açıldığı an /rest/v1/<tablo> uç noktası oluşur. Üçüncüsü, Supabase'in varsayılan grant'ları anon ve authenticated rollerine SELECT, çoğu kurulumda INSERT, UPDATE ve DELETE de verir. Kimse bir şey yapmadan veri dışarıdadır.
Kural
Yedeği public şemasına alma. PostgREST yalnız yayımlanan şemaları görür:
create schema if not exists arsiv; -- PostgREST'e açık değil
create table arsiv.kayitlar_yedek_20260812 as …; -- dışarıdan erişilemez
public'e almak zorundaysan aynı işlemde kapat:
alter table public.kayitlar_yedek enable row level security; -- politika EKLEME
revoke all on public.kayitlar_yedek from anon, authenticated; -- ikinci kilit
Politika eklenmeyen RLS "herkese kapalı" demektir; service_role etkilenmez, yani yedek işlevini korur. Site yalnız okuma yapıyorsa genel sertleştirme de yerinde:
revoke insert, update, delete on all tables in schema public from anon;
alter default privileges in schema public revoke insert, update, delete on tables from anon;
Varsayımla yetinme, sor
Supabase'in güvenlik danışmanı bunu hata seviyesinde raporluyor; ama bakmak gerekiyor. Kesin test veritabanının kendisine sorulur:
select c.relname, c.relrowsecurity,
has_table_privilege('anon', c.oid, 'SELECT') as anon_okuyabilir
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r';
Sonra bir de dışarıdan, gerçek anon anahtarıyla curl at. İçeriden "kapalı" görünen bir şeyin dışarıdan da kapalı olduğunu kanıtlamadan iş bitmez.
Aynı aileden iki tuzak daha
RLS satır politikası kolonları korumaz. for update using (id = auth.uid()) tipi bir politika hangi satırın güncellenebileceğini söyler, hangi kolonun değil. Kullanıcı kendi satırındaki is_admin ya da verification_status gibi bir kolonu PostgREST PATCH ile doğrudan ezebilir; uygulama katmanındaki form yalnız masum kolonları yazıyor olsa bile gerçek sınır politikadır, form değil. Çare tablo düzeyindeki UPDATE yetkisini geri alıp yalnız güvenli kolonlara kolon düzeyinde yetki vermek; ayrıcalıklı kolonlar yalnız security definer bir RPC ya da servis rolüyle değişir.
Ve politika içindeki alt sorgu, baktığı tablonun RLS'ine takılır. "Yazar banlıysa gösterme" diye not exists (select 1 from profiles where … banned_at is not null) yazarsan ama profiles tablosu banlı satırı zaten gizliyorsa alt sorgu o satırı hiç göremez, not exists her zaman doğru döner ve filtre sessizce işlevsiz kalır. Çözüm RLS'i bilerek atlayan küçük bir security definer yardımcı fonksiyon ve politikada onu çağırmak.
Refleks
Bir projede kişisel veri temizliği ya da anonimleştirme yaptıysan işin sonunda şunu ara: temizlenen veri başka bir yerde duruyor mu? Yedek tablo, eski sütun, materialized view, storage bucket, git geçmişi. Temizlik ancak her kopyası gidince tamamlanır.
Bu yazı neye dayanıyor
12 Ağustos 2026, gerçek bir üretim veritabanı. Sabah temizlik, akşam dışarıdan okunabilen yedek. Kolon yetkisi ve alt sorgu tuzakları ise Haziran 2026'da başka bir projenin güvenlik denetiminde yakalandı; ikisi de anon anahtarla canlıda kanıtlandı.
Yazılım ve sistem mimarı, AlgowAI'da COO. Bu yazıdaki her şey canlı bir sistemde ölçülerek yaşandı; tahmin yok. Sorun sende de varsa yaz, bakarım.