Rules Hub
Coding Rules Library
Rule priority, scope & exceptions
Use this to align rules with the senior-level structure (P0/P1/P2, scope, exceptions/tradeoffs).
Block private, loopback, link-local and metadata IP ranges on user-supplied fetch targets
Before fetching a user-controlled URL, resolve the host and reject private/loopback/link-local/CGN/metadata IPs — pin the connection to the validated IP.
Bad example
| 1 | // Hostname blocklist — trivially bypassed. |
| 2 | function isSafe(rawUrl: string): boolean { |
| 3 | const { hostname } = new URL(rawUrl); |
| 4 | const banned = ['localhost', '127.0.0.1', '169.254.169.254']; |
| 5 | return !banned.includes(hostname); |
| 6 | } |
| 7 |
|
| 8 | // Bypassed by: http://internal.attacker.com (A record -> 127.0.0.1), |
| 9 | // http://2130706433/ (decimal IP), http://[::1]/, http://0x7f.0.0.1/ ... |
| 10 | if (isSafe(userUrl)) await fetch(userUrl); |
Explanation (EN)
Filtering on the hostname string is ineffective: attackers use DNS records pointing at internal IPs, decimal/hex/IPv6 encodings, and other representations that the blocklist never enumerates. The request still lands on the metadata service or loopback.
Objašnjenje (HR)
Filtriranje po stringu hosta je neučinkovito: napadači koriste DNS zapise koji pokazuju na interne IP-ove, decimalne/hex/IPv6 enkodiranja i druge reprezentacije koje bloklista ne pokriva. Zahtjev i dalje pogađa metadata servis ili loopback.
Good example
| 1 | import dns from 'node:dns/promises'; |
| 2 | import ipaddr from 'ipaddr.js'; |
| 3 |
|
| 4 | async function resolveSafeIp(rawUrl: string): Promise<string> { |
| 5 | const url = new URL(rawUrl); |
| 6 | if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Bad scheme'); |
| 7 |
|
| 8 | const { address } = await dns.lookup(url.hostname); |
| 9 | const parsed = ipaddr.parse(address); |
| 10 | const range = parsed.range(); // 'private' | 'loopback' | 'linkLocal' | 'carrierGradeNat' | 'unicast' ... |
| 11 | if (range !== 'unicast') throw new Error(`Blocked IP range: ${range}`); |
| 12 | return address; |
| 13 | } |
| 14 |
|
| 15 | const ip = await resolveSafeIp(userUrl); |
| 16 | // Pin the socket to the validated IP so DNS can't be re-resolved to an internal one (TOCTOU/rebinding). |
| 17 | await fetch(userUrl, { /* undici dispatcher / custom lookup pinned to `ip` */ }); |
Explanation (EN)
Validation happens on the resolved IP across all numeric forms, and the connection is pinned to that IP. This blocks RFC1918, 127/8, 169.254/16 (including 169.254.169.254 metadata), ::1, fc00::/7 and CGN ranges regardless of how the host was written, and prevents DNS rebinding between the check and the connect.
Objašnjenje (HR)
Validacija se radi na razriješenom IP-u za sve numeričke oblike, a veza je vezana na taj IP. Ovo blokira RFC1918, 127/8, 169.254/16 (uključujući 169.254.169.254 metadata), ::1, fc00::/7 i CGN raspone bez obzira na zapis hosta, i sprječava DNS rebinding između provjere i spajanja.
Notes (EN)
Per OWASP, also cover the cloud metadata endpoint 169.254.169.254 (and fd00:ec2::254 on AWS IMDSv2 over IPv6); prefer IMDSv2/disable IMDS where possible.
Bilješke (HR)
Prema OWASP-u, pokrijte i cloud metadata endpoint 169.254.169.254 (te fd00:ec2::254 na AWS IMDSv2 preko IPv6); gdje je moguće koristite IMDSv2/isključite IMDS.
Exceptions / Tradeoffs (EN)
Internal service-to-service tooling that intentionally targets private ranges is exempt, but it should still authenticate the upstream and not accept the target from end-user input.
Iznimke / Tradeoffi (HR)
Interni service-to-service alati koji namjerno ciljaju privatne raspone su izuzeti, ali bi i dalje trebali autenticirati upstream i ne primati odredište iz korisničkog unosa.