A service fetches user-supplied URLs and must refuse internal IPv4 destinations; which reserved blocks must its address filter cover, and where do such filters usually leak?
answer
- more than the three private blocks
- registry: not globally reachable
- metadata lives on link-local
- check the address you connect to
basics
~10 sRefuse 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.168.0.0/16, 198.18.0.0/15, the documentation /24s, 224.0.0.0/4 and 240.0.0.0/4, testing the resolved address of every connection, redirects included.
solid answer
~40 sDerive the list from the RFC 6890 registry rather than memory: every block that is not globally reachable, plus multicast and the reserved space. That is `0.0.0.0/8`, the three RFC 1918 blocks, `100.64.0.0/10` shared address space, all of `127.0.0.0/8`, `169.254.0.0/16` link-local (many platforms serve instance metadata there), `192.0.0.0/24`, the three documentation /24s, `198.18.0.0/15` benchmarking, `224.0.0.0/4` multicast and `240.0.0.0/4`, which also covers `255.255.255.255`. Filters leak in predictable places: an "is private" helper that knows only RFC 1918 and misses 100.64.0.0/10 and 169.254.0.0/16; matching `127.0.0.1` instead of the /8; forgetting 0.0.0.0, which some stacks connect to the local host; validating the URL text instead of the parsed address actually connected; and not re-checking after a redirect or a fresh name lookup. An allow-list of permitted destinations is stronger still; this list is the floor.
code
pseudocode · 17 linesBLOCKED = [0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8,
169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24,
192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24,
203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4]
function is_blocked(addr32):
for (network, length) in BLOCKED:
mask = (0xFFFFFFFF << (32 - length)) & 0xFFFFFFFF
if (addr32 & mask) == network:
return true
return false
function safe_connect(host):
addrs = resolve_ipv4(host) # literal or DNS, parsed to 32-bit values
if addrs is empty or any(is_blocked(a) for a in addrs):
refuse
connect_to(addrs[0]) # the address checked, never re-resolvedgo deeper
Know that internal means more than the three RFC 1918 blocks: loopback, link-local and 0.0.0.0 can all reach things inside.
List the blocks from the special-purpose registry and test an address against a prefix with a bitwise AND, including the awkward /10, /12 and /15 boundaries.
Close the leaks: parsed addresses instead of strings, one resolution that is checked and then used, redirects re-checked, link-local metadata blocked, and the IPv6 list kept separately.
Decide where the control lives: a shared egress layer with one maintained list beats every service writing its own check, and an allow-list beats both where the feature permits.
## The goal, stated in protocol terms A server that fetches URLs on a user's behalf (link previews, webhooks, image import) sits inside your network, so it can reach addresses the user cannot. The address filter's job is narrow: **refuse any destination that is not ordinary, globally reachable unicast**. The IANA IPv4 special-purpose registry (RFC 6890, updated by RFC 8190) already says which blocks those are: everything whose **Globally Reachable** value is false. Add multicast, which lives in its own registry, and you have the list. Allow-listing the destinations a feature genuinely needs is the stronger policy; the block list below is the minimum any fetcher should enforce regardless. ## The block list | Block | Name | Why the fetcher must refuse it | |---|---|---| | `0.0.0.0/8` | This host on this network | Not a valid destination; some stacks treat it as the local host | | `10.0.0.0/8` | Private-Use (RFC 1918) | Internal networks | | `100.64.0.0/10` | Shared Address Space (RFC 6598) | Provider-internal; not RFC 1918, so often forgotten | | `127.0.0.0/8` | Loopback | Services bound to the fetcher's own host | | `169.254.0.0/16` | Link Local (RFC 3927) | Same-link services; many platforms serve instance metadata here | | `172.16.0.0/12` | Private-Use (RFC 1918) | 172.16.0.0 to 172.31.255.255 | | `192.0.0.0/24` | IETF Protocol Assignments | Not globally reachable | | `192.168.0.0/16` | Private-Use (RFC 1918) | Internal networks | | `192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24` | Documentation (RFC 5737) | Never a legitimate destination | | `198.18.0.0/15` | Benchmarking | 198.18.0.0 to 198.19.255.255; lab space | | `224.0.0.0/4` | Multicast (RFC 1112) | Group addresses, never a single server | | `240.0.0.0/4` | Reserved | Includes `255.255.255.255`, limited broadcast | Two boundaries to recompute rather than remember: `100.64.0.0/10` fixes the top two bits of the second octet to `01`, so it spans 100.64.0.0 to 100.127.255.255; `198.18.0.0/15` spans two /16s, 198.18 and 198.19. ## Where these filters leak - **"Private" is not "internal".** A helper that tests only the three RFC 1918 blocks passes `100.64.0.0/10`, `169.254.0.0/16`, `127.0.0.2` and `0.0.0.0`. The link-local miss is often the expensive one, because on many platforms the metadata endpoint there can return credentials. - **Single addresses instead of blocks.** Matching `127.0.0.1` leaves 16.7 million other loopback addresses; RFC 1122 reserves the whole `127.0.0.0/8`. - **0.0.0.0 treated as harmless.** RFC 1122 forbids it as a destination on the wire, but some host stacks, Linux among them, treat a connection to it as a connection to the local host. - **Checking text, not the address.** Many address parsers accept forms other than dotted quad (a single decimal number, octal or hex octets) as an implementation convenience. Compare the parsed 32-bit value against the prefixes; never pattern-match strings. - **Check and use drift apart.** A hostname can resolve to an internal address, or resolve differently between the check and the connection. Resolve once, test every returned address, connect to the address you tested, and repeat for every redirect hop. - **Blocks you think you do not use.** Whether `198.18.0.0/15` or `100.64.0.0/10` is live inside your network depends on who built it and which provider carries it, today and after the next migration. Block the whole not-globally-reachable set rather than only the blocks you believe are in use. - **The other address family.** An IPv6 destination needs its own list, including IPv6 forms that embed an IPv4 address; this IPv4 list does not cover it. ## The check as an algorithm 1. Parse the URL and take the host part. 2. Resolve it to addresses; if it is already a literal, parse it to a 32-bit value. 3. Reject if **any** resolved address falls in a blocked prefix (bitwise AND with the prefix mask, compare with the network). 4. Connect to that exact checked address, not to the name again. 5. On a redirect, start again from step 1 with the new URL. ## Keeping the list correct The registry changes rarely but does change: RFC 8190 corrected entries, and RFC 7526 deprecated `192.88.99.0/24`. Treat the IANA registry as the source, give the list an owner, and test it with one address just inside and one just outside each boundary, for example 172.31.255.255 (blocked) and 172.32.0.0 (allowed).
- Why is 100.64.0.0/10 on the list when it is not private space?RFC 6598 reserves it as shared address space for links between carrier-grade NAT and customer equipment, and the registry marks it forwardable but not globally reachable. Inside a provider network, or any network that borrowed it internally, it reaches internal infrastructure, yet an RFC 1918 check passes it.
- Does blocking 240.0.0.0/4 make a separate rule for 255.255.255.255 redundant?Yes. 240.0.0.0/4 runs from 240.0.0.0 to 255.255.255.255, so the limited broadcast address falls inside it. Keeping a named entry can still help readers, but the prefix test already catches it.
- The filter checked the name at request time, yet the fetch hit an internal address. How?The name was resolved twice: once for the check and again by the client library when connecting, and the second answer was internal. The fix is to resolve once, check every returned address, and connect to the checked address directly, repeating the whole check for each redirect.
saying these in an interview costs you the question
- Blocking 10/8, 172.16/12 and 192.168/16 is enough to stop internal fetches.
- Blocking 127.0.0.1 covers loopback.
- 0.0.0.0 cannot reach anything, so it needs no rule.
- 100.64.0.0/10 is public space, so fetching it is safe.
- Validating the hostname string before resolution is sufficient.