Der Security Advisor meldete 34 Warnungen; nach dem Festnageln des search_path sind es zehn. Von diesen zehn ist eine einzige ein echter Befund — aber die hätte man in den 34 nicht gesehen. search_path (34 Warnungen) Alle betroffenen Funktionen sind SECURITY INVOKER, laufen also mit den Rechten der aufrufenden Person; ein manipulierter Pfad bringt dort nichts zu holen. Die vier DEFINER-Funktionen setzen ihn längst. Festgenagelt wird es trotzdem, für den Tag, an dem jemand eine davon auf SECURITY DEFINER umstellt, weil eine Mutation an RLS vorbei schreiben muss — dann wäre es eine Rechteausweitung, und an den search_path denkt in dem Moment niemand. Als Schleife statt als Liste von 34 Signaturen: die würde beim nächsten Umbau veralten. Sie lässt Erweiterungen in Ruhe (pg_trgm legt show_trgm und show_limit ebenfalls in public ab) und prüft am Ende selbst nach. pg_temp steht ausdrücklich am Pfadende — ohne die Angabe durchsucht Postgres das temporäre Schema zuerst, und dort darf jede Sitzung anlegen, was sie will. Ausführungsrechte (8 Warnungen) Hier trennt sich der Befund vom Rauschen, und zwar durch Messen mit dem anon-Schlüssel gegen die laufende Datenbank: anon.rpc(is_hr_user) -> false anon.rpc(current_hr_user_id) -> null anon.rpc(apply_due_pending_changes) -> 0 Die ersten beiden bleiben offen, und das ist keine Nachlässigkeit: sie werden aus den RLS-Policies heraus aufgerufen, und ein Policy-Ausdruck wird mit den Rechten der abfragenden Rolle ausgewertet. Ohne EXECUTE scheitert jede Abfrage auf jeder Tabelle. Preisgegeben wird nichts — beide nehmen keine Argumente und beantworten nur eine Frage über die aufrufende Person selbst. Der dritte ist der Befund. apply_due_pending_changes() wendet vorgemerkte Versetzungen, Beförderungen und Abwesenheiten an, ist SECURITY DEFINER, umgeht damit RLS — und war ohne Anmeldung aufrufbar. Der anon-Schlüssel steht im ausgelieferten Browser-Bündel. Der Schaden wäre begrenzt, weil nur ohnehin fällige Änderungen angewandt werden, aber es ist ein Schreibpfad für Fremde und macht das Geheimnis der Cron-Route wirkungslos. Entzogen für anon und authenticated; die Route benutzt die service_role und läuft weiter. rls_auto_enable() stammt nicht aus diesen Migrationen und wird nirgends aufgerufen. Der Entzug ist risikolos und beantwortet die Frage, was sie tut, notfalls mit einer klaren Fehlermeldung. Zwei Warnungen bleiben bewusst stehen pg_trgm in public trägt die Operatorklasse gin_trgm_ops, auf der zwei GIN-Indizes auf employees liegen. Ein Schemawechsel müsste Indizes und jeden search_path mitziehen — Risiko für eine Konvention, keine Rechteausweitung. „Leaked Password Protection" ist gegenstandslos: die Passwort-Anmeldung ist abgeschaltet, eine Anmeldung gegen die API antwortet mit email_provider_disabled. Es gibt kein Passwort, das kompromittiert sein könnte.
78 lines
3.1 KiB
SQL
78 lines
3.1 KiB
SQL
-- search_path für alle eigenen Funktionen festnageln.
|
|
--
|
|
-- Der Supabase-Linter meldet 34 Funktionen mit „Function Search Path Mutable".
|
|
-- Nachgezählt sind das alles SECURITY-INVOKER-Funktionen; die vier
|
|
-- SECURITY-DEFINER-Funktionen (is_hr_user, current_hr_user_id,
|
|
-- apply_due_pending_changes, fn_track_employee_assignment) setzen den Pfad
|
|
-- längst. Deshalb steht im Advisor auch 0 errors.
|
|
--
|
|
-- Warum das trotzdem behoben wird:
|
|
--
|
|
-- Der Angriff braucht SECURITY DEFINER. Wer in einem Schema, das im
|
|
-- search_path früher liegt, eine eigene Tabelle `employees` anlegt, bringt
|
|
-- eine unqualifiziert schreibende Funktion dazu, auf die untergeschobene
|
|
-- zuzugreifen — mit den Rechten der Eigentümerin der Funktion. Bei INVOKER
|
|
-- läuft alles mit den Rechten der aufrufenden Person, es gibt also nichts zu
|
|
-- gewinnen, und die RLS-Policies greifen unverändert.
|
|
--
|
|
-- Zur Lücke wird die Warnung erst, wenn eine dieser Funktionen später auf
|
|
-- SECURITY DEFINER umgestellt wird, etwa weil eine Mutation an RLS vorbei
|
|
-- schreiben muss. In dem Moment denkt niemand mehr an den search_path.
|
|
-- Einmal festnageln räumt die Falle weg und ändert kein Verhalten.
|
|
--
|
|
-- `pg_temp` steht ausdrücklich am Ende: ohne die Angabe durchsucht Postgres
|
|
-- das temporäre Schema *zuerst*, und dort darf jede Sitzung anlegen, was sie
|
|
-- will.
|
|
|
|
do $$
|
|
declare
|
|
v_func record;
|
|
v_count int := 0;
|
|
begin
|
|
for v_func in
|
|
select p.oid::regprocedure as signature
|
|
from pg_proc p
|
|
join pg_namespace n on n.oid = p.pronamespace
|
|
where n.nspname = 'public'
|
|
-- Nur Funktionen, keine Prozeduren oder Aggregate.
|
|
and p.prokind = 'f'
|
|
-- Erweiterungen gehören uns nicht: pg_trgm legt show_trgm und show_limit
|
|
-- in public ab. Daran zu drehen bricht bei der nächsten Aktualisierung
|
|
-- der Erweiterung oder wird stillschweigend zurückgesetzt.
|
|
and not exists (
|
|
select 1 from pg_depend d where d.objid = p.oid and d.deptype = 'e'
|
|
)
|
|
-- Bereits gesetzte nicht anfassen: die vier DEFINER-Funktionen stehen
|
|
-- auf `search_path = public` und sollen so bleiben.
|
|
and not exists (
|
|
select 1 from unnest(coalesce(p.proconfig, '{}')) c where c like 'search_path=%'
|
|
)
|
|
loop
|
|
execute format('alter function %s set search_path = public, pg_temp', v_func.signature);
|
|
v_count := v_count + 1;
|
|
end loop;
|
|
|
|
raise notice 'search_path festgenagelt für % Funktion(en)', v_count;
|
|
end;
|
|
$$;
|
|
|
|
-- Gegenprobe: danach darf in public keine eigene Funktion ohne search_path
|
|
-- mehr stehen. Schlägt das an, hat die Schleife oben etwas übersehen — besser
|
|
-- hier, als es im Advisor stehen zu lassen.
|
|
do $$
|
|
declare v_offen int;
|
|
begin
|
|
select count(*) into v_offen
|
|
from pg_proc p
|
|
join pg_namespace n on n.oid = p.pronamespace
|
|
where n.nspname = 'public'
|
|
and p.prokind = 'f'
|
|
and not exists (select 1 from pg_depend d where d.objid = p.oid and d.deptype = 'e')
|
|
and not exists (select 1 from unnest(coalesce(p.proconfig, '{}')) c where c like 'search_path=%');
|
|
|
|
if v_offen > 0 then
|
|
raise exception 'Es stehen noch % Funktion(en) ohne search_path in public.', v_offen;
|
|
end if;
|
|
end;
|
|
$$;
|