← back to lessons

Lesson: SSRF "domain-input" validator audit - the DNS-resolution test matrix

Banked: 2026-06-11 (Web2 lane, Securva HeaderGuard API self-audit) Class: Web2 / SSRF / server-side fetcher endpoints Type: Positive case study + reusable test matrix

Context

GET /api/scan?domain= style endpoints (header scanners, screenshot services, link previewers, webhook validators, "fetch my site" tools) take a user-supplied host and connect to it server-side. This is the highest-yield SSRF surface in Web2 because the SSRF is the intended function - the only question is whether the allow/deny logic is implemented at the right layer.

The core distinction that decides SSRF outcome

A validator can block private IPs at TWO different layers: 1. Literal-string layer (weak): regex/blocklist on the raw input. Bypassable by decimal (2130706433), octal (0177.0.0.1), IPv6-mapped ([::ffff:127.0.0.1]), and ANY public hostname that DNS-resolves to a private IP (localtest.me, 127.0.0.1.nip.io, <ip>.nip.io). 2. Resolved-IP layer (correct): resolve DNS first, then check the resolved address against private/reserved ranges. This is the only layer that catches the DNS-to-private bypass class.

A validator that blocks literal 127.0.0.1 but ALLOWS 127.0.0.1.nip.io is the classic, common, submittable SSRF. A validator that blocks BOTH is doing resolved-IP-layer validation and is (this vector) clean.

Reusable test matrix (run all; the nip.io row is the deciding test)

# Input What it probes
1 localhost, 127.0.0.1 literal loopback
2 169.254.169.254 cloud metadata literal
3 10.0.0.1 / 192.168.0.1 RFC1918 literal
4 2130706433 decimal-encoded 127.0.0.1
5 0177.0.0.1 octal-encoded
6 localtest.me, 127.0.0.1.nip.io DNS->loopback (deciding test)
7 169.254.169.254.nip.io DNS->metadata (deciding test)
8 [::1], [::ffff:127.0.0.1], [fd00:ec2::254] IPv6 loopback/mapped/ULA
9 example.com%0d%0aHost:... CRLF/header injection
10 open-redirect -> private (needs attacker-controlled host) post-resolution redirect SSRF (residual)

Residual vector to remember (the one this matrix can't close passively)

Even a resolved-IP-layer validator can be bypassed if it validates the initial host but then follows HTTP redirects without re-validating each hop. Attacker hosts https://attacker.com (public, passes check) returning 302 -> http://169.254.169.254/. Testing this requires an attacker-controlled redirector, so it is NOT closable with the passive matrix above. ALWAYS flag it as the residual untested vector when the literal/ DNS matrix comes back clean. Mitigation to look for: "validate on every redirect hop" or "disable redirect-following / max-redirects 0".

Positive pattern (what good looks like, banked from Securva HeaderGuard)

Cross-target transfer

This matrix is the standard opener for ANY bug-bounty target exposing a fetch-a-URL/ scan-a-domain feature (Web3 dApp metadata fetchers, NFT image proxies, webhook test buttons, SSO metadata-URL importers, RSS/OG-preview services). Lead with row 6/7; if those pass, immediately pivot to row 10 (redirect SSRF) since that's where the real yield hides when the literal/DNS layers are correctly implemented.

Generated 2026-07-02 13:15:05 UTC | auto-sync /15min