Pin search_path, and close a write path that needed no login
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.
This commit is contained in:
@@ -32,8 +32,14 @@ drop table if exists reorg_scenarios;
|
||||
-- Planstelle, und eine offene Stelle ist schlicht eine unbesetzte. Anlegen
|
||||
-- und Schliessen sind deshalb Operationen auf om_positions.
|
||||
|
||||
-- set search_path bei jeder Funktion: siehe die folgende Migration, dort steht
|
||||
-- warum. Kurz: heute sind das INVOKER-Funktionen und der Pfad ist harmlos,
|
||||
-- aber sobald eine davon einmal SECURITY DEFINER wird, wäre er es nicht mehr —
|
||||
-- und daran denkt dann niemand.
|
||||
create or replace function next_position_number()
|
||||
returns text language sql stable as $$
|
||||
returns text language sql stable
|
||||
set search_path = public, pg_temp
|
||||
as $$
|
||||
select '6' || lpad((coalesce(max(substring(position_number from 2)::bigint), 0) + 1)::text, 7, '0')
|
||||
from om_positions
|
||||
where position_number ~ '^6[0-9]{7}$';
|
||||
@@ -43,7 +49,9 @@ comment on function next_position_number() is
|
||||
'Nächste freie Planstellennummer im Nummernkreis 6xxxxxxx.';
|
||||
|
||||
create or replace function create_position(payload jsonb)
|
||||
returns uuid language plpgsql as $$
|
||||
returns uuid language plpgsql
|
||||
set search_path = public, pg_temp
|
||||
as $$
|
||||
declare
|
||||
v_org_unit_id uuid := (payload->>'org_unit_id')::uuid;
|
||||
v_job_title text := nullif(trim(payload->>'job_title'), '');
|
||||
@@ -95,7 +103,9 @@ end;
|
||||
$$;
|
||||
|
||||
create or replace function delete_position(payload jsonb)
|
||||
returns void language plpgsql as $$
|
||||
returns void language plpgsql
|
||||
set search_path = public, pg_temp
|
||||
as $$
|
||||
declare
|
||||
v_position_id uuid := (payload->>'position_id')::uuid;
|
||||
v_label text;
|
||||
|
||||
Reference in New Issue
Block a user