Trumpai
Leidimo patikra SECURITY DEFINER funkcijoje buvo if not (has_perm or created_by = me or assignee_id = me) then raise. 2026 m. birželį visos 3683 programos užduotys neturėjo nei kūrėjo, nei vykdytojo. false OR NULL OR NULL yra NULL, NOT NULL vis dar NULL, o IF praleidžia savo kūną, kai sąlyga yra NULL. Todėl klaida niekada nebuvo iškelta, ir bet kuris prisijungęs vartotojas galėjo klonuoti užduotis. Pataisa: coalesce(..., false) aplink visą sąlygą.
Tai buvo audito įmonės laiko apskaitos programoje, funkcijoje, esančioje už „klonuoti užduotį”.
Kaip atrodė patikra?
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;
Ji skamba kaip „atmesk, nebent galite kurti užduotis, ją sukūrėte arba ji jūsų”. 3683 užduotys buvo užpildytos automatiškai, todėl created_by ir assignee_id visose buvo NULL. Rolei su išjungtais visais leidimais sąlyga buvo NOT (false OR NULL OR NULL), tai yra NULL, ir raise niekada nebuvo įvykdytas.
Kodėl ta pati sąlyga saugi RLS?
Row level security viską, kas nėra true, traktuoja kaip atmetimą, todėl NULL reiškia ne. PL/pgSQL IF viską, kas nėra true, traktuoja kaip „praleisti kūną”, o čia kūnas buvo būtent atmetimas. PostgreSQL dokumentacija nurodo abi puses, row security ir IF; kartu jos tą pačią išraišką vienoje vietoje paverčia draudimu, o kitoje leidimu.
SECURITY DEFINER funkcija taip pat veikia apeidama RLS, todėl galioja tik jos pačios patikra. Užtenka vienos NULL šakos: visos kitos gali būti false.
Kas tai pataisė?
Apgaubkite visą sąlygą taip, kad NULL taptų atmetimu, ir klonavimą traktuokite kaip užduoties kūrimą, o ne šaltinio redagavimą:
if not coalesce((select has_perm('tasks', 'create')), false) then
raise exception 'forbidden' using errcode = '42501';
end if;
Ar tai pasikartojo?
Taip, maždaug po trijų mėnesių, dar trijose funkcijose: užduoties archyvavimas, jos tipo keitimas ir patikra, ar galima joje registruoti laiką. SQL skaitymas jų nepagavo. Pagavo RLS įrodymas pagal roles, nes jis išjungia kiekvieną leidimą ir tikrina, kad veiksmas atmetamas.
Kaip ieškoti?
grep 'if not (' randa sintaksę, o ne klaidą. Kas pasiteisino:
- Viena užklausa su
pg_get_functiondefištraukite visų funkcijų kūnus. - Kiekviename
if not (...) thenirif a <> b thenpašalinkitecoalesce(...),exists(...)ir(select ...). Nuogaorgrandinė ar palyginimas su stulpeliu, galinčiu būti NULL, yra kandidatas. - Taip pat ieškokite
if not <variable> thenir sekite, kur kintamasis nustatomas._ok := has_perm(...) or created_by = me;paslepia tą pačią klaidą dviejose eilutėse. - Kiekvieną pagalbinę funkciją, naudojamą kaip
if not helper(...), iškvieskite trimis būdais atšauktoje transakcijoje: nežinomas id,NULLargumentas ir tikras vartotojas, kurio laukiamos eilutės nėra. Pagalbinei funkcijai, grąžinančiaiNULLbet kuriuo iš jų, reikiacoalescekvietimo vietoje arba viduje.
Toje programoje patikra aprėpė 14 pagalbinių funkcijų, visos jau buvo saugios. Klaida apsiribojo klonavimo funkcija ir trimis vėliau rastomis. Susijusi daugelio nuomininkų spraga: ką RLS praleidžia vartotojui dviejose org.. Programa buvo audito įmonės darbo laiko apskaita; kitos jos tylios klaidos aprašytos straipsnyje darbo laiko apskaita audito įmonei. Supabase saugumo patikra tarp savo 46 taisyklių išvardija SECURITY DEFINER funkcijas, bet negali pasakyti, ar jų viduje esantis IF klaidos atveju praleidžia, todėl ši patikra lieka rankinė.