---
title: "Notater fra en ekstern pentest av en Azure Container Apps-stack"
date: 2026-05-28
description: "Funn og metodikk fra en autorisert ekstern pentest mot en Azure-basert SaaS — WAF-bypass via parallelle ACA-domener, OIDC-scope-misbruk, DMARC-spoofing, og et 'egenutviklet' CRM som viste seg å være en upatchet fork av et open-source-prosjekt."
tags: ["pentest", "azure", "container-apps", "oidc", "security"]
url: https://erik.no/blogg/2026-05-28-notater-fra-en-ekstern-pentest-av-en-azure-container-apps-stack
---

<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>

Jeg har akkurat avsluttet en autorisert ekstern pentest mot en Azure-basert SaaS-plattform hos **Poly AS** (`polyas.no`). Engasjementet var rent eksternt — ingen credentials, ingen auth-fase — så denne posten dekker alt jeg fikk se. Målet er å vise mønstrene og metodikken: fra hvordan WAF-en omgås via ACA sin default-FQDN, til hvordan en `p=none`-DMARC kjeder sammen med over-broad OIDC-scopes til en realistisk tenant-takeover.

<Callout type="info">
  Alt jeg fant og demonstrerer i denne posten er identifisert med utelukkende offentlig tilgjengelig informasjon og standardverktøy — DNS-oppslag, Certificate Transparency, vanlige HTTP-forespørsler, og lesing av åpne metadata. Ingen credentials, ingen interne dokumenter, ingen privilegert tilgang. Hvem som helst med en nettleser og `curl` kan reprodusere det samme. Det er nettopp poenget med en ekstern pentest: å speile hva en angriper kan se uten å ha brutt seg inn først.
</Callout>

## Kontekst

Plattformen kjører på **Azure Container Apps (ACA)** med Next.js på frontend, OIDC mot Entra ID (`polyas.onmicrosoft.com`), et CRM bygget på Better-Auth, en Grafana-instans for intern observability, og en håndfull subdomener bak en WAF. Scope var rent eksternt: ingen credentials, ingen DoS, ingen forsøk på social engineering mot ekte ansatte, og ingen brute-force mot login-skjemaer. UA var satt til en signert pentest-streng på alle prober slik at trafikken kunne korreleres med engagementet.

Et notat om metodikken: dette er et solo-engagement, men ikke 100% manuelt arbeid — og jeg gjemmer ikke at AI gjør mye av tunge-løftet. AI-en skriver custom scripts, orkestrerer recon-pipelines rundt `subfinder` / `dnsx` / `httpx` / `nuclei`, parser `/openapi.json`-differ og `semgrep`-output, foreslår neste angrepsvektor basert på det den har sett, og rydder i Burp-historikk. Jeg styrer retningen, leser resultatene, bryter inn der det trengs, og gjør det som krever menneskelig dømmekraft: scope-grenser, severity, etiske vurderinger, prioritering, og den endelige rapporten. Moderne pentest med AI i loopen — ikke autonomi, og ikke "se så manuell jeg er".

Resten av posten antar at du er kjent med ACA, Entra ID app-registreringer, OIDC PKCE-flyt, og hvordan Microsoft Graph delegated scopes fungerer.

## Recon

Før selve testingen kjørte jeg en bred AI-orkestrert recon-pass — `subfinder` for subdomain-enum, `crt.sh` for cert-historikk, `httpx` for live-status og response-fingerprinting, `nuclei` for kjente fingerprinter. To av kanalene skiller seg ut, fordi de illustrerer hvor lite invasivt det faktisk er:

**Certificate Transparency.** `crt.sh` ga meg en sortert liste av alle subdomener som har hatt et offentlig sertifikat utstedt under apex-domenet de siste tre årene — inkludert flere stagings-/preview-miljøer som ikke var lenket fra hovedsiden. To av dem viste seg å være aktive Container Apps med samme image som produksjon, bare uten WAF.

```bash
curl -s "https://crt.sh/?q=%25.polyas.no&output=json" \
  | jq -r '.[].name_value' \
  | tr ',' '\n' | sort -u
```

**`og:image` og andre metadata.** En triviell wget på forsiden + `grep` etter `azurecontainerapps.io` ga meg den interne ACA-FQDN'en direkte fra `<meta property="og:image">`. Det var den lekkasjen som ble inngangen til funn 1.

```bash
curl -s https://app.polyas.no/ \
  | grep -oE 'https://[a-z0-9-]+\.[a-z0-9-]+\.azurecontainerapps\.io[^"]*'
```

Begge teknikkene er gratis, ikke-invasive, og treffer langt oftere enn man skulle tro.

## 1. ACA-direct ingress bypass

Det mest interessante funnet — og det jeg tror er mest representativt for Azure-stacker generelt.

Når du publiserer en Container App med custom domain (`app.polyas.no`), beholder ACA også den default-genererte FQDN'en på formatet `<app>-<hash>.<region>.azurecontainerapps.io`. Begge svarer med samme applikasjon. Hvis WAF, IP-allowlist eller rate-limiter sitter på `app.polyas.no`, går alt det forbi ved å snakke direkte til `<hash>.azurecontainerapps.io`.

```http
GET / HTTP/1.1
Host: app-abc123.westeurope.azurecontainerapps.io
User-Agent: Mozilla/5.0 (pentest-...)

HTTP/1.1 200 OK
Server: envoy
< samme respons som app.polyas.no, men uten WAF foran >
```

Verifikasjon var enkel — jeg sendte et kjent WAF-trigget pattern (en SQLi-signatur i query-string) mot begge:

```text
GET https://app.polyas.no/?q=' OR 1=1--              → 403 (WAF)
GET https://app-abc123...azurecontainerapps.io/?q=... → 200 (passerer)
```

Funn: <Severity level="high" />.

<Callout type="warning">
  ACA har per i dag ingen innebygd måte å disable default-FQDN'en på. Mitigeringen er enten å sette Front Door eller en annen reverse proxy foran ACA med private ingress (`ingress.allowInsecure=false` + `external=false`), eller å validere `Host`-headeren i applikasjonen og avvise alt som ikke matcher en allowlist av custom domains.
</Callout>

Det samme mønsteret fant jeg på fem av seks publiserte subdomener i denne stacken: CRM, sandbox, observability, en MCP-tjeneste, og en intern API-gateway. Den ene som ikke var sårbar hadde Front Door foran og hadde også gått videre til private ingress på selve ACA-en.

## 2. OIDC-flyten — broad scopes og tx-cookie-rekkefølge

Tenant-portalen bruker Entra ID som identity provider. Authorize-URL'en (sanert) ser slik ut:

```text
https://login.microsoftonline.com/<tenant-guid>/oauth2/v2.0/authorize
  ?client_id=<client-guid>
  &response_type=code
  &redirect_uri=https://app.polyas.no/api/auth/callback
  &response_mode=query
  &scope=openid profile email offline_access
         Mail.ReadWrite Mail.Send
         Files.ReadWrite.All Sites.ReadWrite.All
         Chat.ReadWrite ChannelMessage.Send
  &code_challenge=...&code_challenge_method=S256
  &state=...&nonce=...
```

Tre observasjoner:

**(a) Single-tenant authorize URL.** Tenant-GUID'en sitter i path — ikke `/common/` eller `/organizations/`. Dette er en stille positiv: hele kategorien av Azure AD takeover-bugs som forutsetter multi-tenant authorize URL (typisk CVE-2023-3128-familien for Grafana og andre OSS-apper) gjelder ikke for denne deployen. Verdt å nevne i rapporten fordi det er en konkret arkitektonisk beslutning som har slått ut en kjent angrepsklasse.

**(b) Over-broad Microsoft Graph delegated scopes.** Listen inkluderer `Mail.ReadWrite`, `Mail.Send`, `Files.ReadWrite.All`, `Sites.ReadWrite.All`, `Chat.ReadWrite`, `ChannelMessage.Send` og `offline_access`. Det er tenant-wide write access til mail, OneDrive, SharePoint og Teams under hver bruker som consenter, pluss offline refresh-token for å gjenta det uten at brukeren er til stede.

Konsekvensen: hvis app-secret eller refresh-token lekker fra applikasjonens backend (gjennom logging, en SSRF, en uavhengig kompromittering), oversettes det direkte til "send mail som hvem som helst i tenanten som har logget inn minst én gang". Riktig løsning er **incremental consent**: be om minimumet ved login, så `Mail.Send` (eller hva det nå er) når brukeren faktisk trigger funksjonen som trenger det. Da blir også consent-dialogen forståelig for sluttbrukeren — `Files.ReadWrite.All` på en login-prompt får selv tekniske brukere til å klikke avbryt.

Funn: <Severity level="medium" />.

**(c) tx-cookie valideres før `code`/`state`.** Etter callback gjør appen først en sjekk av `oidc_tx`-cookien (PKCE verifier + nonce + provider-navn), så validerer den `code` og `state`. Det er riktig rekkefølge — en angriper som ikke har tx-cookien fra samme browser session får aldri sjansen til å levere en stjålet `code`. Verdt å nevne både fordi det er sjelden gjort rett, og fordi det betyr at en open redirect alene ikke kan brukes til å stjele auth codes her.

## 3. Auth-bypass på guest-endpoint — en grundig negativ finding

`/api/auth/me` på tenant-portalen returnerer en guest-respons til unauthenticated kallere:

```json
{"displayName": null, "role": "TENANT_USER", "allowedModules": []}
```

Det _ser_ ut som en ubeskyttet endpoint, men er Better-Auth/iron-session SPA-mønsteret: klienten poller denne ruten for å avgjøre om den skal rendre login-knappen eller en authenticated shell. Det er open by design.

Jeg ba AI-en generere testmatrisen og kjøre den gjennom Burp Intruder + en kort Python-wrapper. 15 minutter senere hadde jeg svaret: ingen role-flip-vei. Matrisen:

- **27 trust-header-varianter** — `X-User`, `X-Role`, `X-Forwarded-User`, `X-Forwarded-Email`, `X-Forwarded-Groups`, `X-Original-URL`, `X-Authenticated`, `X-Remote-User`, `X-SSL-Client-DN`, `X-Auth-Request-User`, og 17 til.
- **19 session cookie-navn** — `next-auth.session-token`, `authjs.session-token`, `connect.sid`, `jwt`, `token`, `role`, og diverse interne navn jeg gjettet ut fra `<script>`-bundles.
- **6 Authorization-header-varianter** inkludert JWT `alg=none` med `{"role":"ADMIN"}`-claim, JWT HS256 med dummy-secret, og en raw bearer.
- **6 HTTP-verb** + 3 method-override-headere (`X-HTTP-Method-Override`, `X-Method-Override`, `_method` query).
- **28 path-varianter** — trailing slash, double slash, `..`, `%00`, `.json`, `?role=ADMIN`, `?impersonate=...`.

Alle returnerte identisk guest-body. Det betyr at den realistiske veien til elevated access går gjennom OIDC-flyten selv (typisk refresh-token theft via app-kompromittering), ikke gjennom denne endpointen.

Funn: <Severity level="info" /> — dokumentert som tested and ruled out. Negative findings hører hjemme i rapporten; de viser hva du faktisk har sett på.

<Callout type="info">
  Better-Auth og iron-session bruker begge dette mønsteret med en "ok"- eller "me"-rute som svarer åpent for guests. Hvis du ser noe sånt, ikke fyr av en `<Severity level="high" />` på første sekund — sjekk om det er den dokumenterte SPA-bootstrap-veien først.
</Callout>

## 4. DMARC `p=none` + DNS-hardening

Apex-domenet hadde SPF og DKIM på plass, men DMARC sto på:

```text
v=DMARC1; p=none; sp=none; rua=mailto:...
```

PoC: jeg sendte en spoofet mail fra `security-test@polyas.no` til en egen Hotmail-konto. Den ble levert — i Junk, ikke Inbox, men levert. Det som holdt den ute av Inbox var Outlook sin sender reputation score, ikke DMARC.

```bash
swaks --to tester@example.com \
      --from security-test@polyas.no \
      --header "From: IT Support <security-test@polyas.no>" \
      --server <egen-relay>
```

`p=none` er ikke en DMARC-policy, det er telemetry mode. `sp=none` betyr i tillegg at samme angrep fungerer mot _alle_ subdomener — `payroll.polyas.no`, `it-support.polyas.no`, what have you.

### Den interessante kjeden: intern spoofing → consent-phishing → tenant-wide token

Isolert er DMARC `p=none` en medium finding. Det som gjør den vesentlig verre i denne stacken er at den kombinerer rett i kjeden med funn #2 (over-broad OIDC-scopes). Det var den koblingen jeg ville få fram i rapporten, og som jeg tror er hovedgrunnen til at noen bryr seg om denne posten.

Sett angrepet fra angriperens side:

**Steg 1 — spoofingen fungerer fra eget domene.** Med `p=none` har den mottakende mailserveren ingen policy å enforce på. En spoofet `From: it-support@polyas.no` til `kari.olsen@polyas.no` blir levert. I praksis lander den i Inbox langt oftere når avsender og mottaker deler domene — Outlook scorer slik mail som "internal" hvis det er reputation på domenet fra før, akkurat den scoringen jeg ellers ville klaget på.

**Steg 2 — luren ser ut som en normal IT-melding.**

```text
From: IT Support <it-support@polyas.no>
To:   kari.olsen@polyas.no
Subject: Pålogging må fornyes — handling kreves innen 24 timer

Hei Kari,

Vi har oppgradert Poly Workforce sin Microsoft 365-integrasjon. For å
unngå avbrudd må du re-autorisere appen ved å klikke lenken under:

  https://workforce-renew.polyas-365.com/reauthorize

Med vennlig hilsen,
Mette Andersen
IT Support — Poly AS
```

**Steg 3 — consent-skjermen er ikke mistenkelig.** Lenken peker til en angriperregistrert Entra-app som ber om _akkurat de samme_ scopene som den ekte appen: `Mail.ReadWrite`, `Mail.Send`, `Files.ReadWrite.All`, `Sites.ReadWrite.All`, `Chat.ReadWrite`, `offline_access`. Brukeren har sett denne dialogen før — fra den _ekte_ Poly Workforce-appen. Klikket "Aksepter" er kondisjonert inn. Det er nettopp dette finne #2 koster: når den legitime appen normaliserer en overdreven consent-skjerm, gjør den den maliciøse uskillelig.

```text
https://login.microsoftonline.com/<poly-tenant>/oauth2/v2.0/authorize
  ?client_id=<attacker-app-guid>
  &redirect_uri=https://workforce-renew.polyas-365.com/cb
  &response_type=code
  &scope=Mail.ReadWrite Mail.Send Files.ReadWrite.All
         Sites.ReadWrite.All Chat.ReadWrite offline_access
  &state=...
```

**Steg 4 — refresh-token med tenant-wide write access** under brukerens identitet. Angriperen kan sende mail som offeret (perfekt for å eskalere lateralt med samme spoof-mønster, nå med ekte DKIM-signatur), lese og skrive alle deres OneDrive-filer, alle SharePoint-sites de har tilgang til, og poste i Teams-kanalene de er medlem av — uten at brukeren er til stede, helt til tokenen revokeres manuelt eller refresh-cyclen brytes.

Det er kjeden som gjør at DMARC-funnet ikke kan parkeres på `low`. Det er ikke et selvstendig vulnerability, men det er stykket som gjør hele consent-phishing-angrepet realistisk.

Funn (kjedet): <Severity level="high" />. Isolert DMARC-finding: <Severity level="medium" />.

<Callout type="warning">
  Mitigeringen er flerlags: sett DMARC til `p=quarantine` (helst `reject`) med samme på `sp`, slå på "block legacy authentication" og "require admin consent for high-privilege scopes" i Entra, slå på MFA på consent-flowen, og — viktigst — kutt ned de faktiske scopene i den legitime appen så consent-dialogen ikke lenger ser identisk ut med en credible phishing-app.
</Callout>

### Resten av DNS-en

Mens jeg likevel var i DNS-en, sjekket jeg noen tilstøtende ting:

```text
CAA-records:     ingen
DNSSEC:          ikke aktivert (no DS i parent zone)
MTA-STS:         ingen _mta-sts TXT, ingen policy-fil på mta-sts-host
TLS-RPT:         ingen _smtp._tls TXT
```

Hver av disse er individuelt low severity, men sammen forteller de en historie: ingen har gjort en helhetlig DNS-hardening-pass. CAA hadde stoppet rogue cert-utstedelse, MTA-STS hadde gjort innkommende mail tampering vanskeligere, TLS-RPT hadde gitt detection på MTA-STS-feil.

Funn (resterende DNS-hardening): <Severity level="low" /> samlet.

## 5. CRM-et som var "egenutviklet" — frem til jeg git-clonet det

I salgsmaterialet til Poly AS sin CRM-modul står det "custom-built, designed in-house". Det er noen ganger riktig. Her var det marketing.

### Identifiseringen

To ting fikk meg til å tvile på "egenutviklet"-framstillingen:

**(a) `/openapi.json` ligger åpent** — et bevisst valg, dokumentert i `robots.txt` med en LLM-bot-allowlist. Inni var det rundt 140 endepunkter med en konsistent navngivnings-konvensjon (`/api/v1/<resource>/<action>`), error-format og operasjons-IDer som umiddelbart virket kjente. Det smaker open source, ikke greenfield.

**(b) Bearer-tokenet har et distinkt prefix.** API-nøklene følger formatet `<prefix>_sk_<48 hex>` der prefix-en er en tre-bokstavs streng som er distinkt nok at GitHub-søk på akkurat den strengen treffer ett upstream-repo og noen forks av det.

```bash
gh search code '<prefix>_sk_' --extension ts --limit 10
```

Treff i upstream-organisasjonens repo. Routenavn, error-strings, og hele `/openapi.json`-strukturen matchet 1:1. CRM-et hos Poly AS er en fork av et offentlig CRM-rammeverk, med custom branding og noen integrasjonsmoduler på toppen — men kjernen er upstream-koden, pinned til en commit som ikke er den nyeste. Den eksakte commit-hashen lekkes i `X-Build-SHA`-headeren i hver respons.

```bash
git clone https://github.com/<upstream-org>/<repo>.git
cd <repo>
git checkout <sha-fra-X-Build-SHA>
```

Når koden er offentlig, går arbeidet over fra blackbox-testing til source-assistert pentest. Jeg satte AI-en på å lese gjennom de relevante modulene — auth, file handling, multi-tenant scoping — parallelt med en `semgrep`-pass med standard Node-ruleset. Et knippe kandidater dukket opp raskt. Jeg leste hver av dem manuelt og avgjorde selv hva som var ekte sårbarhet versus false positive.

### Det jeg fant da jeg leste source

Tre vesentlige funn fra repo-en, alle på commit-en som Poly har pinnet:

**(i) Bootstrap-API-nøkkel logges til stdout.** Seed-scriptet som kjøres ved første-deploy genererer en bootstrap-nøkkel hvis miljøvariabelen ikke er satt, og loggfører verdien til stdout. Hvis containerlogger lekker — typisk via observability-pipelinen, eller hvis noen henter ACA log streams via Azure Portal mot en under-privilegert rolle — kan den nøkkelen havne et sted den ikke skal være. Jeg kunne ikke verifisere at det skjer i Poly sin konkrete deploy uten en konto, men hypotesen er konkret nok til å sjekke i log-aggregatoren.

**(ii) Path traversal i fil-upload-håndtereren.** Et `POST`-endepunkt tar et `filename`-felt rett fra request body og bruker det i `path.join(uploadDir, filename)`:

```ts
// upstream, sanert
const filePath = path.join(UPLOAD_DIR, body.filename);
await fs.writeFile(filePath, buffer);  // ingen path-normalisering
```

`path.join("/uploads", "../../../etc/passwd")` blir til `/etc/passwd`. Det ligger en draft-fix i et åpent upstream-issue, men den er ikke merget — og pinned-commit-en er pre-fix. Auth-gated, men kombinert med kjeden fra funn #4 (consent-phishing → token) er det en plausibel angrepsvei.

**(iii) Manglende tenant-isolasjon på `/api/v1/customers/<id>`.** Endepunktet sjekker bearer-token, men ikke at `<id>` tilhører samme tenant som tokenet. Klassisk IDOR uten tenant-skall. Fix-en i upstream er én linje — `WHERE tenantId = :tenant` på query-en — men den linjen finnes ikke i den pinnede commit-en. Med en gyldig token for tenant A kan man lese kunder i tenant B.

Funn (kjedet etter token-tyveri): <Severity level="high" />.

Funn (manglende patch-monitoring mot upstream sine security advisories): <Severity level="medium" />.

<Callout type="warning">
  "Custom-built" som forretningsuttalelse er nesten alltid en fork pluss noen tilpasninger. Det er ikke et problem i seg selv — open source er greit. Problemet er når forken aldri synces mot upstream sine security advisories, og når kunden tror de kjører proprietær kode med dedikert sikkerhetsarbeid bak. Audit-spørsmålet er ikke "er det egenutviklet?", men "hvor ofte synkroniserer dere mot upstream sin security branch, og hvem eier den prosessen?"
</Callout>

## 6. CSP-nonces uten CSP-header

CRM-frontenden satte en ny `nonce`-attributt på hver `<script>`-tag per request — pent, og et tegn på at noen hadde tenkt på CSP. Problemet:

```http
HTTP/1.1 200 OK
Content-Type: text/html
< ingen Content-Security-Policy-header >
...
<script nonce="aB3kQp...">...</script>
```

Nonces uten en policy som krever dem er dekorasjon. Enhver injisert `<script>` uten nonce vil kjøre.

Jeg tippet i rapporten at årsaken var en reverse proxy nedstrøms som strippet headeren, ikke en designbeslutning — kombinasjonen "appen genererer nonces per request" og "responsen mangler header" ser ut som et deployment-problem. Verdt å sjekke `nginx`/Front Door-konfigen for `proxy_hide_header` eller tilsvarende.

Funn: <Severity level="low" />.

## 7. Grafana — version-vulnerable, men auth-gated

`observability.polyas.no` viste seg å være Grafana OSS `11.6.14` plain — uten `+security-01`/`+security-04`-bundles. Med versjonen pinnet kunne jeg krysse av 11 CVE-er som teknisk gjelder.

Den eksternt eksploiterbare delmengden er null. Hver eneste PoC jeg kjørte returnerte HTTP 401:

```bash
# Eksempel: editor-IDOR (sanert versjon)
curl -i https://observability.polyas.no/api/dashboards/uid/<uid> \
     -H "User-Agent: Mozilla/5.0 (pentest-...)"
# HTTP/1.1 401 Unauthorized
```

Native signup er stengt på API-laget (`POST /api/user/signup` → `401 "User signup is disabled"`), så den eneste veien til en konto er Azure AD SSO mot Poly sin tenant — som er kundens M365-tenant, tredjepart og out of scope.

Det er én skjult variabel her: hvis `[auth.azuread] allow_sign_up = true` står i `grafana.ini`, så vil enhver bruker i `polyas.onmicrosoft.com` som logger inn via SSO bli auto-provisioned en Grafana-konto. Den config-biten er ikke observerbar utenfra. Anbefalingen i rapporten var: audit `grafana.ini`, eller provisjoner en least-privilege Viewer for testeren slik at Editor-CVE-ene kan demonstreres konkret.

Funn: <Severity level="info" /> ekstern, med deferred-liste på auth-fasen.

## 8. Småting verdt å notere

- **`/api/health` lekker miljø.** Flere subdomener returnerer `{"service":"...","version":"dev"}` på `/api/health`. Det er ingen sårbarhet alene, men `"version":"dev"` på en prod-host antyder at environment-variablene ikke er strengt separert mellom build targets. Verdt å rapportere som en config hygiene-finding.
- **Misvisende cookie-navn.** OIDC tx-cookien heter `*_admin_oidc_tx` på en endpoint som ikke har noen admin-flyt — bare en `provider=tenant-entra`-rute. Det er legacy fra en tidligere arkitektur. Misvisende navn kommer alltid til å være en angrepsoverflate fordi de inviterer feil mental modell hos den neste utvikleren som rører kode.
- **Sandbox-/test harness publisert med produksjons-DNS.** En egen "mini-integration test harness" på samme apex som produksjon, med banner som sa "not for production". Den var åpen mot internett og hadde samme ACA-bypass-pattern. Reduserer ikke risiko fordi det står "test" på den.

## Hva jeg lærte

- **Azure Container Apps gir deg en gratis WAF-bypass hvis du ikke vet om den.** Hver ACA-app jeg har sett siden har samme mønster — sjekk dine egne `*.azurecontainerapps.io`-FQDNer.
- **OIDC consent-screens er en del av angrepsflaten.** "Vi ber om scopes vi kan trenge senere" er ikke gratis — det er en blanco-fullmakt til hele tenanten. Incremental consent finnes for en grunn.
- **Severity er en funksjon av kjeder, ikke isolerte funn.** DMARC `p=none` alene er medium. Sammen med en consent-dialog som er konditionert til å se urimelig ut, blir det en realistisk vei til tenant-wide token theft.
- **"Custom-built" trigger refleksen min, ikke en aha-opplevelse.** Det betyr nesten aldri "ikke open source", og jeg tror aldri på den påstanden før jeg har fingerprintet. Bruk ti minutter på det før du tror på marketing — `/openapi.json`-strukturer, token-prefiks, error-strings og bundle-hasher ligger åpent. Er koden en fork, har du upstream sin issue-tracker som gratis vuln-database.
- **Negative findings er findings.** Auth-bypass-runden på `/api/auth/me` ga ingen kompromittering, men 96 forsøk dokumentert i rapporten gjør at kunden vet hva som er testet.
- **Decorative security er verre enn ingen security**, fordi den fjerner risikoen fra både utviklerens og reviewerens radar. CSP-nonces uten policy er det klareste eksempelet.
- **Ekstern angrepsoverflate er ikke "skjult" av at den er ukjent for forsvareren.** Alt i denne posten ligger åpent på internett akkurat nå. Threat-actors driver med Certificate Transparency, `og:image`-scraping og DNS-recon i industriell skala — det er ikke en spesialist-disiplin. Hvis du ikke har sett dine egne assets fra utsiden nylig, er det noen andre som har.

## Det jeg ikke fikk testet

Scope inkluderte ingen credentials, og det kom ingen auth-fase. Det er verdt å være eksplisitt om hva det betyr at en rapport ikke kan si noe om — både for kunden og for fremtidige lesere som vurderer en lignende avgrensning:

- **Misbruk av broad-scope-consenten end-to-end.** Om en SSRF, en logging-lekkasje eller en library-CVE i selve applikasjonen kan brukes til å hente ut det aktive refresh-tokenet og handle som hvilken som helst innlogget bruker, er ikke observerbart utenfra. Det jeg vet er at _hvis_ et token lekker, så er konsekvensene tenant-bred skrivetilgang.
- **`returnTo`-redirect-allowlisten på post-auth-flowen.** Klassiske huller (`//evil.example`, `https://app.polyas.no.evil.example`, path-confusion) krever en gyldig session for å trigge — uten en konto er det bare statisk JS-analyse, og det er ikke nok til å rapportere noe konkret.
- **De 10 Editor-required og 1 Viewer-required Grafana-CVE-ene.** Alle elleve er version-vulnerable per docs, men ingen kan demonstreres uten minst en Viewer-konto i Grafana-instansen.
- **Live verifikasjon av CRM-funnene.** Path traversal, tenant-IDOR og bootstrap-key-lekkasjen fra funn #5 er identifisert fra source, ikke demonstrert mot Poly sin deploy — siste milliliter krever en gyldig bearer-token. I tillegg er role-bypass, mass-assignment og prompt-injeksjon i AI-featurene utestet. Det er den største blindsonen i rapporten, og dekkes bare av en oppfølgende internal eller assumed-breach-engagement.

En ekstern-bare scope finner det som ligger eksponert. Den finner ikke det som ligger bak login — og i en moderne SaaS er det som regel der det meste av angrepsflaten faktisk bor. Det er en god ting å være tydelig på i konklusjonen, ikke en svakhet å skjule.
