Hopp til hovedinnhold
Erik Nilsen
17 min lesing

Notater fra en ekstern pentest av en Azure Container Apps-stack

  • #pentest
  • #azure
  • #container-apps
  • #oidc
  • #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.

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.

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.

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.

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.

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.

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:

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

Funn: High.

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.

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:

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

{"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-varianterX-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-navnnext-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: Info — dokumentert som tested and ruled out. Negative findings hører hjemme i rapporten; de viser hva du faktisk har sett på.

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.

4. DMARC p=none + DNS-hardening

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

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

PoC: jeg sendte en spoofet mail fra [email protected] 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.

swaks --to [email protected] \
      --from [email protected] \
      --header "From: IT Support <[email protected]>" \
      --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: [email protected] til [email protected] 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.

From: IT Support <[email protected]>
To:   [email protected]
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.

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): High. Isolert DMARC-finding: Medium.

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.

Resten av DNS-en

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

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): 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.

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.

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

// 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): High.

Funn (manglende patch-monitoring mot upstream sine security advisories): Medium.

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

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

# 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/signup401 "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: Info ekstern, med deferred-liste på auth-fasen.

8. Småting verdt å notere

  • /api/health lekker miljø. Flere subdomener returnerer {"service":"...","version":"dev"}/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.