Server-side request forgery () is "an attack vector that abuses an application to interact with the internal/external network or the machine itself" (SSRF Prevention Cheat Sheet, Context). Every defence against it, like every egress allow-list, must decide whether the address behind a Uniform Resource Locator (URL) is a public destination. The Internet Assigned Numbers Authority () keeps the map: special-purpose registries whose rows cite (RFC) documents, plus address-space registries for multicast and the space the Internet Engineering Task Force () reserves. By the end you can read the registries, classify a normalised address, show how public destinations lead inside, list the metadata addresses and test a classifier.

Two registries, five columns

RFC 6890 sets the scope: "The IPv4 Special-Purpose Address Registry records all IPv4 special-purpose address blocks" (RFC 6890, section 2.2); an IPv6 registry does the same. Each row has a Classless Inter-domain Routing () block and five Boolean columns (section 2.2.1): Source and Destination, valid at either end of a datagram crossing two devices; Forwardable, whether a router may forward it between external interfaces; Reserved-by-Protocol, handled specially by IP itself; Globally Reachable, whether the datagram "is forwardable beyond a specified administrative domain" (RFC 8190, section 2.1). IANA warns: "Address prefixes listed in the Special-Purpose Address Registry are not guaranteed routability in any particular local or global context" (IANA IPv4 registry).

The IPv4 map

The registry's values (IANA IPv4 registry); multicast comes from the address-space registry, where 224/8 to 239/8 are RESERVED (IANA IPv4 Address Space registry):

BlockNameDestinationGlobally ReachableRFC
0.0.0.0/8"This network"FalseFalseRFC 791
0.0.0.0/32"This host on this network"FalseFalseRFC 1122
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16Private-UseTrueFalseRFC 1918
100.64.0.0/10Shared Address SpaceTrueFalseRFC 6598
127.0.0.0/8LoopbackFalseFalseRFC 1122
169.254.0.0/16Link LocalTrueFalseRFC 3927
192.0.0.0/24IETF Protocol AssignmentsFalseFalseRFC 6890
192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24DocumentationFalseFalseRFC 5737
198.18.0.0/15BenchmarkingTrueFalseRFC 2544
224.0.0.0/4Multicastno rowno rowRFC 1112
240.0.0.0/4ReservedFalseFalseRFC 1112
255.255.255.255/32Limited BroadcastTrueFalseRFC 919

RFC 791 defines the zero network: "A value of zero in the network field means this network" (RFC 791, section 3.2). Erratum 6404 to RFC 6890 split its old entry into the first two rows (IANA IPv4 registry). RFC 1122 allows the /32 only as a source, while a host learns its address, and says of loopback: "Addresses of this form MUST NOT appear outside a host" (RFC 1122, section 3.2.1.3).

Link-local hosts pick from 169.254.1.0 to 169.254.254.255, and "A router MUST NOT forward a packet with an IPv4 Link-Local source or destination address" (RFC 3927, sections 2.1 and 7); 169.254.169.254 sits there. Shared Address Space serves carrier-grade network address translation (), and "packets with Shared Address Space source or destination addresses MUST NOT be forwarded across Service Provider boundaries" (RFC 6598, sections 1 and 6); a metadata service answers inside it too.

192.0.0.0/24 is usable only through more specific reservations, two of them the globally reachable anycast addresses 192.0.0.9/32 and 192.0.0.10/32 (IANA IPv4 registry). "Addresses within the TEST--1, TEST-NET-2, and TEST-NET-3 blocks SHOULD NOT appear on the public Internet" (RFC 5737, section 4). Benchmarking starts at 198.18.0.0; printed 192.18.0.0, which verified erratum 423 corrects (RFC 2544, erratum 423). A /10 leaves 22 host bits, 2 to the power 22 or 4,194,304 addresses, so 100.64.0.0/10 ends at 100.127.255.255; the CIDR / Subnet Calculator prints such edges but does not classify.

The IPv6 map

The IPv6 rows (IANA IPv6 registry), with multicast and site-local from the address-space registry (IANA IPv6 Address Space registry):

BlockNameDestinationGlobally Reachable
::1/128Loopback AddressFalseFalse
::/128Unspecified AddressFalseFalse
::ffff:0:0/96IPv4-mapped AddressFalseFalse
64:ff9b::/96IPv4-IPv6 Translat.TrueTrue
64:ff9b:1::/48IPv4-IPv6 Translat.TrueFalse
2001:db8::/32, 3fff::/20DocumentationFalseFalse
fc00::/7Unique-LocalTrueFalse
fe80::/10Link-Local UnicastTrueFalse
ff00::/8Multicastno rowno row
fec0::/10Reserved by IETFno rowno row

Packets to ::1 must never leave the node, nor link-local ones the link (RFC 4291, sections 2.5.3 and 2.5.6). Unique local space serves a site or a few, with locally assigned prefixes in fd00::/8 (RFC 4193, sections 1 and 3.1). Documentation prefixes "MUST NOT be used for actual traffic, MUST NOT be globally advertised" (RFC 9637, section 4). The well-known prefix for translation between IPv6 and IPv4 (), 64:ff9b::/96, may not represent non-global IPv4 addresses (RFC 6052, section 3.1).

RFC 4291's type rule misleads: "implementations must treat all addresses that do not start with any of the above-listed prefixes as Global Unicast addresses" (RFC 4291, section 2.4). Unique local, documentation and 64:ff9b:1::/48 are Global Unicast by type yet not globally reachable, and new implementations must treat the deprecated fec0::/10 as Global Unicast (section 2.5.7): filter on reachability, not type. The IPv6 Toolkit decodes addresses, IPv4-mapped forms included, and labels special use, but lacks rows such as 3fff::/20, 2001::/23, 64:ff9b:1::/48, 5f00::/16 and 100:0:0:1::/64, and labels by type, so 3fff::1 reads as global unicast.

Special-purpose is not the same as internal

Membership is not the verdict: five IPv4 and nine IPv6 special-purpose blocks are Globally Reachable (IANA IPv4 and IPv6 registries). Nor is every other address public: "While the assumption that a non-special-use address is reachable from the global Internet is generally safe, it is not always true" (RFC 6598, section 5.1). An Amazon Web Services () virtual private cloud () may use publicly routable space (Amazon VPC User Guide), and many protected networks guard non-private addresses (Jackson et al., section 5.3).

So a public destination can still lead inside: its address is in a network you protect, its name resolves inside at connection time, or its server redirects inside. An allow-list of intended destinations answers the first (SSRF Prevention Cheat Sheet, Case 1); the next sections answer the others.

Normalise, then classify

The Uniform Resource Identifier (URI) grammar allows only dotted decimal for IPv4, yet routines such as inet_aton() accept far more (RFC 3986, sections 3.2.2 and 7.4): shorter forms such as a.b.c or one 32-bit number, with octal (leading 0) or hexadecimal (leading 0x) parts, so that the manual's own example turns 0x7f.1 into 127.0.0.1 (inet(3)). getaddrinfo() reads numeric hosts the same way (getaddrinfo(3)), the URL Standard's IPv4 parser reads hexadecimal and octal (URL Standard, section 3.5), and IPv6 receivers must accept every legitimate spelling (RFC 5952, section 4). Python's ipaddress parses as strictly as inet_pton(), refusing leading zeros since 3.9.5 (Python ipaddress documentation); socket.inet_aton() does not. Illustrative session, Python 3.13.16 on Linux:

>>> import ipaddress, socket
>>> ipaddress.ip_address("0x7f.1")
Traceback (most recent call last):
  ...
ValueError: '0x7f.1' does not appear to be an IPv4 or IPv6 address
>>> socket.inet_ntoa(socket.inet_aton("0x7f.1"))
'127.0.0.1'
>>> socket.inet_ntoa(socket.inet_aton("2130706433"))
'127.0.0.1'

For illustration, the URL Inspector follows RFC 3986 and reads http://2130706433/ as a registered name, where Node's new URL() returns 127.0.0.1. RFC 3986's defence: "literals should be converted to numeric form and filtered based on the numeric value, and not on a prefix or suffix of the string form" (RFC 3986, section 7.4); the cheat sheet's: "Treat parser disagreement as a rejection" (SSRF Prevention Cheat Sheet, Case 1).

IPv6 can carry the address too: ::ffff:7f00:1 and 64:ff9b::7f00:1 both hold 127.0.0.1. Since that can fool IPv4 firewalls, "administrators should assure that packets that send to or from IPv4-embedded IPv6 addresses are subject to the same filtering as those directly sent to or from the embedded IPv4 addresses" (RFC 6052, section 5.3). Translators must drop packets that pair the well-known prefix with a non-global IPv4 address (section 3.1), a rule that does not cover 64:ff9b:1::/48 (RFC 8215, section 5): extract the IPv4 address and classify it as IPv4.

Resolve once, connect to what you checked

Checking a name is not checking a connection. In DNS rebinding, "the attacker rebinds the host name to the IP address of a target server that is inaccessible from the public Internet" (Jackson et al., section 1), and on a server "A second, unchecked DNS lookup between validation and connection can bypass these checks" (SSRF Prevention Cheat Sheet, Case 1). Resolve once, check every A and answer, and connect to a checked address, keeping the hostname for the Host header, TLS Server Name Indication and certificate checks, on retries and fallbacks too (same cheat sheet). curl pins like this:

curl --resolve example.com:443:192.0.2.10 https://example.com

--resolve makes curl use that address for that host and port (curl manual page). Redirects, a 3xx with a Location inside, are the other way in: the cheat sheet connects only to the validated address with redirects disabled (SSRF Prevention Cheat Sheet, Case 2), and curl follows one only with -L or --location, --proto-redir limiting its protocols (curl manual page). Refuse redirects or check every hop.

The metadata addresses

In a cloud the prize is the instance metadata service (SSRF Prevention Cheat Sheet). AWS's Instance Metadata Service () holds the instance role's temporary credentials with no authentication or cryptographic protection (AWS, Use instance metadata). The attack class is a feature that fetches a user-supplied URL, such as an image download, pointed at the service (Alibaba Cloud, Instance metadata). The addresses:

ProviderAddressesA request must carrySource
AWS169.254.169.254, fd00:ec2::254a session token, when tokens are requiredaddresses, tokens
Google Cloudmetadata.google.internal, metadata.goog, 169.254.169.254, fd20:ce::254 for IPv6-only virtual machinesMetadata-Flavor: Googleoverview, querying
Azure169.254.169.254Metadata: trueservice page
Alibaba Cloud100.100.100.200a token in Security Hardening Mode; none by defaultmetadata page

AWS says "These IP addresses are link-local addresses and are valid only from the instance" (AWS, Access instance metadata), but the registry puts fd00:ec2::254 and fd20:ce::254 in unique local fc00::/7 (IANA IPv6 registry), and 100.100.100.200 is in 100.64.0.0/10: blocking only 169.254.0.0/16 and fe80::/10 passes three of the four.

With Instance Metadata Service Version 2 (IMDSv2), tokens come from a PUT and last one second to six hours; tokenless requests get 401 Unauthorized when tokens are required, a PUT with X-Forwarded-For is rejected, and the PUT response has a hop limit of 1 by default (AWS, How IMDSv2 works). Google and Azure reject X-Forwarded-For too (Google Cloud, querying metadata; Microsoft Learn, Azure IMDS). Headers are no wall, since many SSRF flaws let attackers set arbitrary headers (AWS Security Blog, 2019): Google says to sandbox processes that should not reach the server (Google Cloud, metadata overview), Azure suggests local firewall rules (Microsoft Learn), and AWS can switch the endpoint off (AWS, Modify instance metadata options):

# On an AWS instance: create an IMDSv2 token, then send it with the request
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/

# Require tokens, switch the endpoint off, read the settings
aws ec2 modify-instance-metadata-options --instance-id i-1234567890abcdef0 --http-tokens required --http-endpoint enabled
aws ec2 modify-instance-metadata-options --instance-id i-1234567890abcdef0 --http-endpoint disabled
aws ec2 describe-instances --instance-id i-1234567898abcdef0 --query 'Reservations[].Instances[].MetadataOptions'

Testing a classifier

Python's ipaddress defines is_private and is_global by the IANA registries, with exceptions, and "is_private has value opposite to is_global, except for the shared address space (100.64.0.0/10 range) where they are both False" (Python ipaddress documentation). Mapped addresses are judged by their IPv4 address, which ipv4_mapped extracts; nothing extracts it from 64:ff9b::/96, and is_reserved is not Reserved-by-Protocol (same documentation). Illustrative output, Python 3.13.16:

>>> import ipaddress
>>> cols = ("is_private", "is_global", "is_reserved", "is_loopback",
...         "is_link_local", "is_multicast")
>>> for s in ("192.0.2.10", "100.64.0.1", "169.254.169.254", "224.0.0.1",
...           "240.0.0.1", "::ffff:7f00:1", "64:ff9b::7f00:1",
...           "fd00:ec2::254", "ff02::1"):
...     a = ipaddress.ip_address(s)
...     print(f"{s:16}", *(f"{getattr(a, c)!s:5}" for c in cols),
...           getattr(a, "ipv4_mapped", ""))
...
192.0.2.10       True  False False False False False
100.64.0.1       False False False False False False
169.254.169.254  True  False False False True  False
224.0.0.1        False True  False False False True
240.0.0.1        True  False True  False False False
::ffff:7f00:1    True  False False True  False False 127.0.0.1
64:ff9b::7f00:1  False True  True  False False False None
fd00:ec2::254    True  False False False False False None
ff02::1          False True  False False False True  None

Each line holds an address, its six flags and its mapped IPv4 address. 100.64.0.1 is neither private nor global, so if ip.is_private: deny admits Shared Address Space and Alibaba's metadata address; 224.0.0.1, ff02::1 and 64:ff9b::7f00:1 (127.0.0.1 inside) are global, so if not ip.is_global: deny, the cheat sheet's sample test (SSRF Prevention Cheat Sheet, Case 1), admits multicast and NAT64-embedded loopback unless is_multicast and the embedded address are checked too.

A library's flag is not the registry. In illustrative runs of Python 3.13.16 and 3.14.2, is_global contradicts the 2025-10-09 registries on four rows, three allocated in 2024 and 2025: 5f00::1, 100:0:0:1::1 and 192.88.99.2 come out global though their rows are not, and 2001:1::3 not global though its row is (IANA IPv4 and IPv6 registries). The flags have also changed three times for IPv4-mapped addresses alone, in 3.11, 3.13 and 3.14 (Python changelog): pin the version and test it.

The Python playground runs CPython 3.14.2 through Pyodide; in an illustrative run its ipaddress matched 3.13.16, but its getaddrinfo() failed on 0x7f.1, so use it for ipaddress only. A test plan:

  1. Test the first and last address of every row in the four registries; a deny wherever Destination or Globally Reachable is False, and for multicast and reserved space.
  2. Repeat each denied address in every spelling the client accepts: dotted, integer, hexadecimal, octal and short IPv4; dotted and hexadecimal IPv4-mapped; uncompressed and upper-case IPv6; a zone identifier; 64:ff9b::/96 and 64:ff9b:1::/48 forms.
  3. Resolve a name with several A and AAAA answers: every answer checked, the connection made to a checked one.
  4. Serve a redirect to a denied address.
  5. Re-run after every library or registry change.

The SSRF URL classifier is one quick check on a single URL: it decodes decimal, octal, hexadecimal and short IPv4 hosts and the dotted IPv4-mapped form, and recognises 169.254.169.254, 100.100.100.200, fd00:ec2::254, metadata.google.internal and metadata, but it never resolves names or sends requests, so steps 3 and 4 are beyond it.

What to remember

  • Decide by the columns: deny a destination whose Destination or Globally Reachable value is False; some special-purpose blocks are globally reachable.
  • Multicast (224.0.0.0/4, ff00::/8) and IETF-reserved IPv6 space live in the address-space registries.
  • Treat 100.64.0.0/10 as internal: it is not globally reachable and holds Alibaba Cloud's metadata address, 100.100.100.200.
  • Classify the number the client will connect to, IPv4 inside IPv6 included, and reject a host that parsers read differently.
  • Resolve once, check every answer, connect to the checked address, and refuse redirects or check every hop.
  • The metadata addresses are 169.254.169.254, fd00:ec2::254, fd20:ce::254 and 100.100.100.200, and the two IPv6 ones are unique local.
  • A library's flag is not the registry: Python's flags disagree on 100.64.0.0/10, call multicast global and contradict four rows.

Sources