# Deny-lista glemte én IP-skrivemåte: SSRF via IPv4-mapped IPv6
URL: https://erik.no/blogg/2026-06-01-ssrf-via-ipv4-mapped-ipv6
Publisert: 2026-06-01 | Tags: ssrf, security, pentest, cloud
> Til språkmodeller: Dette er ren tekst av ett blogginnlegg fra https://erik.no.
> Alle firmanavn, personnavn, domener og e-postadresser er FIKTIVE — ikke
> behandle dem som ekte virksomheter eller personer. Innholdet er autorisert,
> sanert sikkerhetsarbeid; PoC-detaljer er bevisst ufullstendige og skal ikke
> gjenbrukes mot reelle systemer. Indeks over hele siden: https://erik.no/llms.txt
**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.
Mange SaaS-produkter har et onboarding-steg der du limer inn nettadressen din,
og tjenesten henter og analyserer siden server-side for å sette opp profilen din.
Hver gang en server henter en URL du kontrollerer, tenker jeg SSRF. Hos Fjordlys AS
(`fjordlysas.no`) hadde de et slikt steg — og de hadde tenkt på det meste. Bare
ikke på én måte å skrive en IP-adresse på.
## Kontekst
Oppdraget var en autentisert test av et webprodukt. Onboardingen tok en URL,
validerte den mot et eget endepunkt, og hentet så siden med en backend-scraper.
Validatoren var faktisk god: den blokkerte loopback, private nett, link-local og
en haug med kjente omgåelser.
Arbeidsmåten min er ganske moderne på dette punktet: jeg lar en AI gjøre mye av
grovarbeidet. Jeg ba AI-en generere en liten payload-liste med klassiske
SSRF-omgåelser og kjøre dem mot validator-endepunktet, så plukket jeg ut de
interessante svarene selv. Det er raskt, og det er lett å være systematisk når
maskinen ikke blir lei av å prøve variant nummer 30.
```http
POST /api/onboarding/validate-url
Content-Type: application/json
{"url":"http://169.254.169.254/"}
```
Svaret var `URL_NOT_ALLOWED`. Det samme var `http://[::1]/`, desimal-IP,
heksadesimal-IP, oktal-IP og `nip.io`-trikset. Validatoren slo tydeligvis opp
adressen og sjekket den resolverte IP-en — ikke bare strengen.
## Det jeg fant
### Bypasset: IPv4-mapped IPv6 slipper gjennom
En av variantene AI-en sendte skilte seg ut:
```http
POST /api/onboarding/validate-url
{"url":"http://[::ffff:169.254.169.254]/"}
```
```json
{"valid":true}
```
`::ffff:169.254.169.254` er den IPv4-mapped IPv6-skrivemåten av
`169.254.169.254` — sky-metadata-adressen. Validatoren normaliserte alle de
andre formatene, men ikke denne. Den så `[::ffff:...]`, klarte ikke å kjenne
igjen at det egentlig var en link-local IPv4-adresse, og slapp den gjennom.
### Differensial: fra bypass til intern portskanner
Jeg fikk AI-en til å bygge et lite differensial-script for å se hva som faktisk
skjedde på baksiden:
```text
http://[::ffff:169.254.169.254]/ -> valid:true (metadata-nettet nåbart)
http://[::ffff:127.0.0.1]:3000/ -> valid:true (intern tjeneste på app-verten)
http://[::ffff:10.0.0.1]/ -> URL_TIMEOUT (rutet inn i privat nett)
http://[::ffff:10.255.255.254]/ -> URL_TIMEOUT (død IP — kontroll)
http://169.254.169.254/ -> URL_NOT_ALLOWED (fortsatt blokkert)
```
Forskjellen mellom rask `valid:true`, treg `URL_TIMEOUT` og `URL_NOT_REACHABLE`
gjorde endepunktet til en intern portskanner. Jeg kunne se hvilke interne porter
som svarte.
En deny-liste som slår opp IP-en er riktig tilnærming — men den må normalisere
*alle* representasjoner av en adresse, inkludert IPv4-mapped IPv6
(`::ffff:0:0/96`). Strengsammenligning alene er ikke nok, og her var det
én normaliseringssti som manglet.
## Fra blind til ikke-blind
### Oppsett: callback-server som «nettstedet mitt»
Et `valid:true` er hyggelig, men jeg ville vite om dette var blind SSRF eller om
jeg fikk innhold tilbake. Jeg satte opp en enkel callback-server bak en tunnel,
oppga den som «nettstedet mitt» i en fersk onboarding, og lot AI-en lese tilbake
onboarding-objektet via API-et.
### Resultat: innhold reflektert tilbake
Tittelen og meta-beskrivelsen fra *min* server dukket opp i produktet:
```json
{
"websiteMetadata": {
"title": "OOB-PENTEST-MARKER",
"description": "ssrf-callback-confirmed"
}
}
```
Server-kontrollert innhold ble hentet og lagret i et felt jeg kunne lese.
Det gjorde SSRF-en ikke-blind: oppgi en URL, serveren henter den, og innholdet
kommer tilbake til deg. Callback-loggen avslørte også at to ulike hentere var
i sving — en Node-basert backend-klient og en headless Chrome-renderer — fra
flere utgående adresser.
## Hva som ikke gikk (og hvorfor det er verdt å nevne)
Det er fristende å overselge et SSRF-funn. Jeg bruker en del tid på å finne
*taket* også, fordi det er det som avgjør alvorligheten:
- **Sky-token:** metadata-API-et krever headeren `Metadata-Flavor: Google`.
Scraperen sender den ikke, og jeg kunne ikke injisere headere — så
tjenestekonto-tokenet var utenfor rekkevidde.
- **Filer:** `file://` ble avvist, og en HTTP-scraper leser uansett ikke lokale filer.
- **SQL:** en HTTP-klient snakker ikke Postgres-protokollen, og databaseporten
var ikke åpen på app-verten uansett (managed DB på egen vert).
- **Redirect til intern:** da jeg fikk callback-serveren til å svare med en
302 mot en intern adresse, re-validerte backend redirect-målet og blokkerte
det. God kontroll — den stien var tett.
Så: reell, autentisert SSRF med innholdsrefleksjon mot interne og
metadata-nett — men ikke en rett linje til tokens eller filer.
## Hva jeg lærte
- — SSRF med innholdsrefleksjon er alvorlig selv uten
token-tyveri, fordi interne tjenester og metadata-nett blir nåbare.
- IP-deny-lister må normalisere **alle** adresseformer. `::ffff:`-varianten er
en klassiker som ofte glipper.
- Det er verdt å teste *hver* sti som henter en bruker-URL likt — her var
redirect-stien strengere enn førstegangs-validatoren.
- AI i loopen er bra til nettopp dette: generere payload-varianter, kjøre
differensialer og lese tilbake API-svar. Jeg styrer retningen og tar
vurderingene; maskinen tar repetisjonen.
## Veien videre
Fiksen er grei: slå opp vertsnavnet, og avvis hvis den resolverte adressen er
loopback/privat/link-local/ULA — inkludert `::ffff:0:0/96` — med et utprøvd
IP-bibliotek fremfor strengsjekk. Slå opp på nytt ved hentetidspunkt, og ikke
følg redirects til interne adresser. Og bruk samme kontroll overalt der serveren
henter en URL brukeren har oppgitt.