Pereiti prie turinio

NULL PL/pgSQL IF NOT praleido patikrą 3683 užduotims

Postgres

Žiūrėti kaip MarkdownAtidaryti per ClaudeAtidaryti per ChatGPTAtidaryti per Perplexity

Klausimas taip pat nukopijuojamas. Jei pokalbis atsidaro tuščias, įklijuokite jį.

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.

Tas pats NULL, du priešingi atsakymai. has_perm yra false, created_by ir assignee_id yra NULL. RLS politikoje: false OR NULL → NULL → Eilutė atmesta; IF NOT (...) THEN RAISE atveju: false OR NULL → NOT NULL yra NULL → RAISE praleistas → Klonuoti leista.
Trijų reikšmių logika, kaip ją aprašo PostgreSQL 18 dokumentacija, skyriai 5.9 (row security) ir 41.6.4 (IF).

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:

  1. Viena užklausa su pg_get_functiondef ištraukite visų funkcijų kūnus.
  2. Kiekviename if not (...) then ir if a <> b then pašalinkite coalesce(...), exists(...) ir (select ...). Nuoga or grandinė ar palyginimas su stulpeliu, galinčiu būti NULL, yra kandidatas.
  3. Taip pat ieškokite if not <variable> then ir sekite, kur kintamasis nustatomas. _ok := has_perm(...) or created_by = me; paslepia tą pačią klaidą dviejose eilutėse.
  4. Kiekvieną pagalbinę funkciją, naudojamą kaip if not helper(...), iškvieskite trimis būdais atšauktoje transakcijoje: nežinomas id, NULL argumentas ir tikras vartotojas, kurio laukiamos eilutės nėra. Pagalbinei funkcijai, grąžinančiai NULL bet kuriuo iš jų, reikia coalesce kvietimo 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ė.

© 2026 Denys Zaichenko. Originalus straipsnis: https://madsalt.tech/lt/notes/plpgsql-if-not-null-fails-open/