Skip to content

Writing/Securitate

Cum auditezi o aplicație Supabase cu propria ei cheie

Cheia anon pleacă în fiecare build. Înainte de o lansare o iau, o îndrept spre baza de date și trec prin fiecare tabel și fiecare funcție întrebând ce îi spun unui străin.

ISIonuț Sandu19 aug 2026 · 9 min de cititSecuritateSupabasePostgres

Cheia anon pleacă în bundle-ul aplicației. O are oricine descarcă aplicația, ceea ce înseamnă că fiecare întrebare pe care cheia are voie să o pună e o întrebare pe care are voie să o pună publicul. Majoritatea dezvoltatorilor de pe stiva asta știu asta la modul abstract. Mult mai puțini s-au așezat efectiv cu cheia în mână să vadă la ce poate răspunde.

Așa că acum e un pas pe lista de dinainte de orice lansare: extrag cheia din propriul build, o îndrept spre baza de date fără nicio sesiune atașată și parcurg toată suprafața. Ce urmează e lista de verificat și cele două lucruri pe care le prinde cel mai des.

Un tabel fără row-level security

Primul e un tabel din schema public cu row-level security niciodată pornit. PostgREST publică tot ce e în schema aia, deci un tabel neprotejat e un endpoint public. Aproape niciodată nu e o politică scrisă greșit. E o politică niciodată scrisă, de obicei pe un tabel adăugat de o integrare, nu de mână: un jurnal de evenimente webhook, un jurnal de sincronizare, o pistă de audit.

Exact alea sunt tabelele la care nimeni nu se gândește ca la date de utilizator și exact alea se dovedesc a ține id-uri de utilizator, id-uri de produs și payloaduri brute. Puse cap la cap, un jurnal de webhook de facturare enumeră fără probleme cine sunt clienții tăi plătitori și ce au cumpărat.

Interogarea care le găsește e o singură instrucțiune și merită rulată pe fiecare proiect, în loc să raționezi despre ce tabele ar putea avea nevoie. O gaură într-un tabel cu patru rânduri e aceeași gaură pe care o va avea la patruzeci de mii, iar numărul merge într-o singură direcție.

audit.sqlTS
-- every public table with RLS still switched offselect tablename from pg_tables t where schemaname = 'public' and not rowsecurity; -- and every function anon may executeselect p.proname, p.prosecdef  from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public'   and has_function_privilege('anon', p.oid, 'execute');

Pentru un tabel în care scrie doar un webhook, soluția e RLS pornit fără nicio politică, ceea ce pare o greșeală și nu e. Cheia de service-role ocolește RLS complet, deci cel care scrie nu e afectat, iar niciun drum din client n-ar trebui să citească tabelul în primul rând. Lasă un comentariu pe tabel care spune asta, țintit exact spre cine va fi tentat mai târziu să adauge o politică de client ca să meargă ceva.

O hartă de identitate în ambele sensuri, publicată ca API

Al doilea e mai urât și mult mai ușor de ratat, pentru că nu e un tabel. Orice aplicație cu invitații trebuie să transforme undeva o adresă de email într-un id de utilizator. Transformarea aia e de obicei o funcție în baza de date, e de obicei SECURITY DEFINER ca să poată vedea schema de autentificare, iar dreptul de EXECUTE pe ea e de obicei acordat lui PUBLIC, pentru că așa face generatorul.

PostgREST publică fiecare funcție publică drept RPC. La fel și funcțiile ajutătoare de lângă ea, cele scrise pentru politicile RLS și niciodată gândite să fie apelate direct. Înlănțuiește trei sau patru dintre ele și ai o căutare de la email la id de utilizator, inversul ei și tot ce mai întorc ajutătoarele alea: nivel de abonament, apartenență la grupuri. Între ele, o hartă de identitate în ambele sensuri peste toată baza de utilizatori, la care se poate răspunde fără sesiune.

“O funcție ajutătoare scrisă pentru o politică RLS e internă exact până când PostgREST decide că e API.”

Niciuna dintre ele nu e ingenioasă și tocmai ăsta e punctul. Amândouă sunt setări implicite pe care nu le-a revizuit nimeni: un tabel adăugat de o integrare înainte ca RLS să devină reflex și un drept venit odată cu un șablon. Vulnerabilitățile ingenioase sunt rare. Setările implicite sunt peste tot.

Ce i-aș spune oricui e pe aceeași stivă

Nu audita codul. Auditează suprafața. Scoate cheia anon din propriul build, îndreapt-o spre producție și treci prin fiecare tabel și fiecare funcție întrebând ce îți spune. Durează o după-amiază și e singura variantă a exercițiului care găsește ceva, pentru că cititul codului îți spune ce ai vrut, iar cheia îți spune ce ai livrat.

Al doilea obicei e mai mic și a prins mai multe: presupune că fiecare funcție din schema public e un API public, pentru că este. Dacă n-ar trebui să fie, nu are ce căuta în public, iar faptul că e o funcție ajutătoare nu schimbă nimic.

Ionuț Sandu — 19 aug 2026Copiază linkul

URMĂTORUL · 14 AUG 2026 · 7 MIN DE CITIT

Un rând care ține de doi oameni, o ștergere care ține de unul

Orice aplicație ajunge la un moment dat să aibă un buton de ștergere a contului, de obicei pentru că îl cere un magazin. Într-o aplicație în care un rând descrie doi oameni, butonul ăla e o problemă de modelare a datelor deghizată în setare.

Preiau proiecte noi

Spune-mi ce construiești.Un plan scris și un preț fix înainte să se miște vreun ban.

Răspund la tot într-o zi lucrătoare. Dacă nu sunt omul potrivit pentru proiect, ți-o spun din start, nu după trei săptămâni.