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