Härdning av hela TÜV-revisionen
Tio av tolv fynd stängda, två reducerade med skälen utskrivna. Varje
stängning prövas av integrationssviten mot riktig Postgres — 144
kontroller, upp från 100.
T-1 Händelsenyckeln är nu (arende_id, id). Det finns inget delat
namnrum kvar att ockupera, så attacken saknar yta i stället för att
vara mildrad. Inom ett ärende bedöms en kollision på klientens
avtryck — en hash av det kanoniserade innehåll klienten skickade,
taget före serverns egna fält och före krypteringen, eftersom
varken raden eller nyttolasten går att jämföra. Identiskt innehåll
är fortfarande idempotent; samma id med annat innehåll ger 409 och
skriver ingenting alls, inte heller resten av satsen. Samma attack
en nivå upp — ärende-id är lika förutsägbart — stängs separat: ett
id som ägs av en annan organisation ger 409 i stället för att tyst
låta bli, vilket tidigare lämnade offrets ärende oskapat och varje
senare synk svarande 404.
T-2 Mätdonet slås upp i registret. Beteckning och kalibrering HÄRLEDS
därifrån och skriver över det klienten skickade. Okänt mätdon ger
400. Utgånget avvisas inte — mätningen gjordes — men registrets
datum följer med, så graderingen faller på registrets uppgift. Ett
mätvärde utan mätdon får sina påstådda uppgifter borttagna.
T-3 Reducerad, inte stängd. Personnycklarna kuverteras under en
huvudnyckel utanför databasen. En återställd dump ger nycklar som
inte öppnas — verifierat genom att starta om utan huvudnyckeln. Vad
som återstår står utskrivet: en backup tagen FÖRE en radering, plus
huvudnyckeln, återställer fortfarande uppgifterna.
Därtill: gallringen verkställs nu av ett eget jobb och grupperar på det
blindade fordonsindexet så att en delad nyckel inte gallras för tidigt
(T-4) · AI-avlästa mätvärden bär härkomst (T-5) · bcrypt kostnad 12
(T-6) · åtkomstloggen och raderingsregistret är append-only, med en
smalare regel för personnycklarna som måste kunna förstöras (T-7) ·
åtkomstloggen dokumenterad (T-8) · ett externt regelpaket utan signatur
spärrar avslut (T-9) · CORS faller inte längre öppet (T-10) · exp krävs
i token (T-11) · react-router 7 (T-12, med den kvarvarande avvikelsen
motiverad).
Den motspelande hyresgästen som revisionen efterlyste finns nu som
testform och körs i CI.
---- Vad härdningen själv avslöjade -----------------------------------
Två av rättelserna var kortvarigt fel på samma sätt som fynden, och
bägge fångades bara av att jag försökte bevisa dem:
T-7-testet var grönt mot en TOM tabell. En radnivåtrigger har inga
rader att fyra på, så delete lyckades och kontrollen mätte ingenting.
T-6-testet påstod anropet, inte kostnaden. toContain("gen_salt('bf')")
hade accepterat kostnad 6 för evigt — och gjorde det, så länge det
fanns.
Bägge är mönstret revisionen namngav: en kontroll som är riktig i sina
egna termer och oprövad vid sin gräns. Det gäller tester lika mycket som
kod.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -105,17 +105,49 @@ create table if not exists bilage_innehall (
|
||||
skapad timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---- Händelseloggen -----------------------------------------------------
|
||||
--
|
||||
-- Nyckeln är (arende_id, id) och INTE id ensamt. Skillnaden är hela
|
||||
-- TÜV-revisionens T-1.
|
||||
--
|
||||
-- Klienten sätter id, och gjorde det som Date.now() + en räknare — alltså
|
||||
-- förutsägbart. Med en global primärnyckel och `on conflict do nothing`
|
||||
-- kunde en organisation skriva de id:n en ANNAN organisation snart skulle
|
||||
-- använda, till sitt eget ärende, och den andras händelse föll tyst bort.
|
||||
-- Servern svarade 200. Teknikern såg händelsen på skärmen, ärendet
|
||||
-- stängdes, och felorsaken fanns inte i loggen.
|
||||
--
|
||||
-- Append-only skyddade det som skrevs. Ingenting skyddade det som
|
||||
-- hindrades från att skrivas. Med nyckeln skopad till ärendet finns det
|
||||
-- inget delat namnrum kvar att ockupera.
|
||||
create table if not exists felsokning_handelser (
|
||||
id text primary key,
|
||||
id text not null,
|
||||
arende_id text not null references felsokning_arenden(id),
|
||||
tidpunkt timestamptz not null,
|
||||
anvandare text not null,
|
||||
handelse jsonb not null,
|
||||
insatt timestamptz not null default now()
|
||||
insatt timestamptz not null default now(),
|
||||
primary key (arende_id, id)
|
||||
);
|
||||
create index if not exists felsokning_handelser_arende_idx
|
||||
on felsokning_handelser (arende_id, tidpunkt);
|
||||
|
||||
-- Migrering av en befintlig installation: den gamla nyckeln på enbart id
|
||||
-- byts mot den skopade. Kollisioner mellan ärenden kan inte uppstå vid
|
||||
-- bytet — den gamla nyckeln var strängare — så bytet är alltid möjligt.
|
||||
do $$
|
||||
begin
|
||||
if exists (
|
||||
select 1 from pg_constraint c
|
||||
join pg_class t on t.oid = c.conrelid
|
||||
where t.relname = 'felsokning_handelser' and c.contype = 'p'
|
||||
and (select count(*) from unnest(c.conkey)) = 1
|
||||
) then
|
||||
alter table felsokning_handelser drop constraint felsokning_handelser_pkey;
|
||||
alter table felsokning_handelser add primary key (arende_id, id);
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
create or replace function forbjud_andring() returns trigger
|
||||
language plpgsql as $$
|
||||
begin
|
||||
@@ -340,3 +372,54 @@ drop trigger if exists fakturahandelser_append_only on fakturahandelser;
|
||||
create trigger fakturahandelser_append_only
|
||||
before update or delete on fakturahandelser
|
||||
for each row execute function forbjud_andring();
|
||||
|
||||
-- ---- Underlagen om åtkomst och radering (TÜV T-7) ----------------------
|
||||
--
|
||||
-- Fyra tabeller bar append-only-skydd: loggen, ärendet, fakturorna och
|
||||
-- fakturahändelserna. Utelämnandena var påfallande — det var just de
|
||||
-- tabeller som UTGÖR bevisningen om dataskydd som gick att skriva om.
|
||||
--
|
||||
-- Åtkomstloggen är beviset för att åtkomststyrningen fungerade. Den är
|
||||
-- det första en granskare ber om efter en incident, och den enda handling
|
||||
-- som kunde ändras av den som hade motiv att göra det.
|
||||
drop trigger if exists atkomstlogg_append_only on atkomstlogg;
|
||||
create trigger atkomstlogg_append_only
|
||||
before update or delete on atkomstlogg
|
||||
for each row execute function forbjud_andring();
|
||||
|
||||
-- Beviset för att en radering verkställdes fick inte självt kunna raderas.
|
||||
drop trigger if exists raderingar_append_only on raderingar;
|
||||
create trigger raderingar_append_only
|
||||
before update or delete on raderingar
|
||||
for each row execute function forbjud_andring();
|
||||
|
||||
-- Personnycklarna behöver radering — det är vad radering ÄR — så här går
|
||||
-- det inte att förbjuda. Regeln är i stället smalare: nyckeln och
|
||||
-- raderingsbegäran får ändras, identiteten och tillhörigheten inte.
|
||||
-- Annars kunde en nyckel flyttas till ett annat subjekt och en radering
|
||||
-- träffa fel person.
|
||||
create or replace function skydda_personnyckel() returns trigger
|
||||
language plpgsql as $$
|
||||
begin
|
||||
if TG_OP = 'DELETE' then
|
||||
return old;
|
||||
end if;
|
||||
if (new.id, new.organisation_id, new.subjekt, new.skapad)
|
||||
is distinct from
|
||||
(old.id, old.organisation_id, old.subjekt, old.skapad) then
|
||||
raise exception 'En personnyckels identitet och tillhörighet kan inte ändras';
|
||||
end if;
|
||||
return new;
|
||||
end $$;
|
||||
|
||||
drop trigger if exists personnycklar_skydd on personnycklar;
|
||||
create trigger personnycklar_skydd
|
||||
before update or delete on personnycklar
|
||||
for each row execute function skydda_personnyckel();
|
||||
|
||||
-- Avtryck av det klienten faktiskt skickade, före serverns egna fält och
|
||||
-- före krypteringen (TÜV T-1). Utan det går en kollision inte att bedöma:
|
||||
-- serverns tidsstämpel skiljer sig alltid vid en omsändning, och
|
||||
-- krypteringen ger ny chiffertext för samma klartext, så varken raden
|
||||
-- eller nyttolasten kan jämföras. Avtrycket kan.
|
||||
alter table felsokning_handelser add column if not exists klientdigest text;
|
||||
|
||||
Reference in New Issue
Block a user