Bug-ul
O aplicație de facturare făcută cu un AI builder își ține datele în Supabase. Utilizatorii care își deschid facturile primesc „permission denied”. Fondatorul lipește eroarea în AI builder, care scrie o nouă politică de row-level security, iar eroarea dispare. Condiția politicii este using (true): orice rând, pentru oricine. În funcție de rolul pe care se aplică, asta înseamnă orice utilizator logat sau oricine are cheia publică a aplicației.
Cum se vede
Arată ca o reparație. Facturile se încarcă, eroarea a dispărut, fondatorul merge mai departe. În aplicație, fiecare utilizator vede tot doar facturile lui, pentru că doar pe acelea le cer ecranele. Cine interoghează baza de date direct, cu cheia pe care browserul o are deja, le primește pe ale tuturor. În istoricul proiectului, schimbarea apare ca bug fix.
Cum îl găsește un test
Creezi doi utilizatori de test, fiecare cu o factură. Te loghezi ca A și ceri facturile prin API-ul public, cum face browserul, dar fără filtrul aplicației. Numeri rândurile care îi aparțin lui B. Orice număr peste zero înseamnă test picat. O a doua verificare citește lista de politici din baza de date și semnalează orice politică pe un tabel cu date de utilizator a cărei condiție e pur și simplu true.
Reparația
Politica se înlocuiește cu una limitată la proprietar: auth.uid() = user_id. Apoi se caută cauza reală a erorii inițiale. De obicei e un rând inserat fără ID de utilizator, pe care politica pe proprietar nu avea cum să-l potrivească.
Testul care îl prinde
Un test cu doi utilizatori: A cere facturile lui B prin API-ul public, cu cheia anon și token-ul lui A, și așteaptă zero rânduri. O interogare în CI pe pg_policies oprește build-ul dacă vreo politică de pe un tabel cu date de utilizator are condiția doar true.
import { test, expect } from "@playwright/test";
import { anonClientSignedInAs, seed, sql } from "./db";
test("user A reads none of user B's invoices", async () => {
const supabase = await anonClientSignedInAs(seed.userA);
const { data } = await supabase
.from("invoices")
.select("id")
.eq("user_id", seed.userB.id);
expect(data ?? []).toHaveLength(0);
});
test("no policy on a user-data table is just `true`", async () => {
const open = await sql`
select tablename, policyname from pg_policies
where schemaname = 'public' and qual = 'true'`;
expect(open).toEqual([]);
});
Ce pune în pericol
Facturile tuturor clienților, cu nume, adrese și sume, la îndemâna oricărui utilizator al aplicației. E o încălcare GDPR, iar în aplicație nu se vede nimic.
