TL;DR
SECURITY DEFINER -funktion sisällä oleva oikeustarkistus oli muotoa if not (has_perm or created_by = me or assignee_id = me) then raise. Kesäkuussa 2026 sovelluksen kaikilta 3683 tehtävältä puuttui sekä tekijä että vastuuhenkilö. false OR NULL OR NULL on NULL, NOT NULL on edelleen NULL, ja IF ohittaa runkonsa, kun ehto on NULL. Virhettä ei siis koskaan nostettu, ja kuka tahansa kirjautunut käyttäjä pystyi kloonaamaan tehtäviä. Korjaus on coalesce(..., false) koko ehdon ympärillä.
Tämä tapahtui tilintarkastusyhtiön ajanseurantasovelluksessa, “kloonaa tehtävä” -toiminnon takana olevassa funktiossa.
Miltä tarkistus näytti?
if not ( has_perm('tasks', 'create')
or v_task.created_by = auth.uid()
or v_task.assignee_id = auth.uid() ) then
raise exception 'forbidden';
end if;
Se luetaan “torju, ellei sinulla ole oikeutta luoda tehtäviä, et ole tehnyt tätä etkä omista sitä”. Nuo 3683 tehtävää oli luotu automaattisesti, joten created_by ja assignee_id olivat NULL jokaisella. Roolille, jonka kaikki oikeudet olivat pois päältä, ehdosta tuli NOT (false OR NULL OR NULL), joka on NULL, eikä raise koskaan suoritettu.
Miksi sama ehto on turvallinen RLS:ssä?
Row level security käsittelee kaiken, mikä ei ole true, torjuntana, joten NULL tarkoittaa ei. PL/pgSQL:n IF käsittelee kaiken, mikä ei ole true, tulkintana “ohita runko”, ja tässä runko oli itse torjunta. PostgreSQL-dokumentaatio kertoo molemmat puolet, row securityn ja IF-lauseen osalta; yhdessä ne tekevät samasta lausekkeesta torjunnan yhdessä paikassa ja sallimisen toisessa.
SECURITY DEFINER -funktio myös ajetaan RLS:n ohi, joten sen oma tarkistus on ainoa, joka pätee. Yksi NULL-haara riittää: muut haarat voivat kaikki olla false.
Mikä korjasi sen?
Kääri koko ehto niin, että NULL muuttuu torjunnaksi, ja kohtele kloonausta tehtävän luomisena, älä lähteen muokkaamisena:
if not coalesce((select has_perm('tasks', 'create')), false) then
raise exception 'forbidden' using errcode = '42501';
end if;
Tapahtuiko se uudelleen?
Kyllä, noin kolme kuukautta myöhemmin, vielä kolmessa funktiossa: tehtävän arkistoinnissa, sen tyypin vaihtamisessa ja sen tarkistamisessa, saako siihen kirjata aikaa. SQL:n lukeminen ei löytänyt niitä. Roolikohtainen RLS-todistus löysi, koska se kytkee jokaisen oikeuden pois päältä ja tarkistaa, että toiminto torjutaan.
Miten koodi käydään läpi?
grep 'if not (' löytää syntaksin, ei bugia. Tämä toimi:
- Vedä jokaisen funktion runko yhdellä kyselyllä käyttäen
pg_get_functiondef. - Poista jokaisesta
if not (...) thenjaif a <> b then-kohdastacoalesce(...),exists(...)ja(select ...). Paljasor-ketju tai vertailu nullable-sarakkeen yli on ehdokas. - Etsi myös
if not <variable> thenja jäljitä, missä muuttuja asetetaan._ok := has_perm(...) or created_by = me;kätkee saman bugin kahdelle riville. - Kutsu jokaista apufunktiota, jota käytetään muodossa
if not helper(...), kolmella tavalla peruutettavassa transaktiossa: tuntematon id,NULL-argumentti ja oikea käyttäjä, jolta odotettu rivi puuttuu. Apufunktio, joka palauttaaNULLmissä tahansa näistä, tarvitseecoalesce-kutsun kutsukohdassa tai sisällä.
Siinä sovelluksessa läpikäynti tarkisti 14 apufunktiota, ja kaikki olivat jo turvallisia. Bugi rajoittui kloonausfunktioon ja kolmeen myöhemmin löytyneeseen. Aiheeseen liittyvä monivuokraajaongelma: mitä RLS ei huomaa kahden organisaation käyttäjän kohdalla. Sovellus oli tilintarkastusyhteisön tuntiseurantatyökalu; sen muut hiljaiset virheet ovat kirjoituksessa tuntiseuranta tilintarkastusyhteisölle. Supabase tietoturvatarkistus listaa SECURITY DEFINER -funktiot 46 sääntönsä joukossa, mutta se ei voi kertoa, avautuuko niiden sisällä oleva IF auki virhetilanteessa, joten tämä läpikäynti pysyy käsityönä.