A falsificação de requisição do lado do servidor (, server-side request forgery) é "an attack vector that abuses an application to interact with the internal/external network or the machine itself", a aplicação vira arma contra a rede ou a máquina (SSRF Prevention Cheat Sheet, seção Context). Toda defesa contra ela, como toda allow-list de saída, decide se o endereço por trás de uma URL (Uniform Resource Locator) é um destino público. A Internet Assigned Numbers Authority () mantém o mapa: registros de propósito especial cujas linhas citam (RFC) e registros de espaço de endereçamento para o multicast e o espaço que a Internet Engineering Task Force () reserva. Ao final, você consegue ler os registros, classificar um endereço normalizado, mostrar como destinos públicos levam para dentro, listar os endereços de metadados e testar um classificador.

Dois registros, cinco colunas

A RFC 6890 define o escopo: "The IPv4 Special-Purpose Address Registry records all IPv4 special-purpose address blocks", todos os blocos especiais estão nele (RFC 6890, seção 2.2); um registro IPv6 faz o mesmo. Cada linha traz um bloco em notação Classless Inter-domain Routing () e cinco colunas booleanas (seção 2.2.1): Source e Destination, se o endereço vale como origem ou destino de um datagrama entre dois dispositivos; Forwardable, se um roteador o encaminha entre interfaces externas; Reserved-by-Protocol, se o próprio IP o trata à parte; Globally Reachable, se o datagrama "is forwardable beyond a specified administrative domain", passa de um domínio administrativo (RFC 8190, seção 2.1). A IANA avisa: "Address prefixes listed in the Special-Purpose Address Registry are not guaranteed routability in any particular local or global context", o registro não garante rota (registro IPv4 da IANA).

O mapa IPv4

Os valores do registro (registro IPv4 da IANA); o multicast vem do registro de espaço de endereçamento, onde 224/8 a 239/8 constam como RESERVED (registro IPv4 Address Space da IANA):

BlocoNomeDestinationGlobally 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/4Multicastsem linhasem linhaRFC 1112
240.0.0.0/4ReservedFalseFalseRFC 1112
255.255.255.255/32Limited BroadcastTrueFalseRFC 919

A RFC 791 define a rede zero: "A value of zero in the network field means this network", zero é esta rede (RFC 791, seção 3.2). A errata 6404 da RFC 6890 dividiu a antiga entrada nas duas primeiras linhas (registro IPv4 da IANA). A RFC 1122 só aceita o /32 como origem, enquanto o host aprende o próprio endereço, e diz do loopback: "Addresses of this form MUST NOT appear outside a host", nunca fora do host (RFC 1122, seção 3.2.1.3).

No link-local, os hosts escolhem entre 169.254.1.0 e 169.254.254.255, e "A router MUST NOT forward a packet with an IPv4 Link-Local source or destination address", roteador não encaminha (RFC 3927, seções 2.1 e 7); o 169.254.169.254 fica ali. O Shared Address Space serve à tradução de endereços de rede () da operadora, e "packets with Shared Address Space source or destination addresses MUST NOT be forwarded across Service Provider boundaries", não cruzam provedores (RFC 6598, seções 1 e 6); um serviço de metadados também responde ali.

O 192.0.0.0/24 só vale por reservas mais específicas, e duas delas, os endereços anycast 192.0.0.9/32 e 192.0.0.10/32, são globalmente alcançáveis (registro IPv4 da IANA). "Addresses within the TEST--1, TEST-NET-2, and TEST-NET-3 blocks SHOULD NOT appear on the public Internet", fora da internet pública (RFC 5737, seção 4). O benchmarking começa em 198.18.0.0; a imprimiu 192.18.0.0, e a errata 423, verificada, corrige (RFC 2544, errata 423). Um /10 deixa 22 bits de host, 2 elevado a 22 ou 4.194.304 endereços, então 100.64.0.0/10 termina em 100.127.255.255; a CIDR / Subnet Calculator mostra essas bordas, mas não classifica.

O mapa IPv6

As linhas do registro IPv6 (registro IPv6 da IANA), com multicast e site-local do registro de espaço de endereçamento (registro IPv6 Address Space da IANA):

BlocoNomeDestinationGlobally 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::/8Multicastsem linhasem linha
fec0::/10Reserved by IETFsem linhasem linha

Pacotes para ::1 nunca saem do nó, nem pacotes link-local saem do link (RFC 4291, seções 2.5.3 e 2.5.6). O unique local serve a um site ou a poucos, com prefixos locais em fd00::/8 (RFC 4193, seções 1 e 3.1). Prefixos de documentação "MUST NOT be used for actual traffic, MUST NOT be globally advertised", sem tráfego real nem anúncio global (RFC 9637, seção 4). O prefixo bem conhecido da tradução entre IPv6 e IPv4 (), 64:ff9b::/96, não pode representar IPv4 não global (RFC 6052, seção 3.1).

A regra de tipo da RFC 4291 engana: "implementations must treat all addresses that do not start with any of the above-listed prefixes as Global Unicast addresses", o resto é Global Unicast (RFC 4291, seção 2.4). Unique local, documentação e 64:ff9b:1::/48 são Global Unicast pelo tipo sem serem globalmente alcançáveis, e implementações novas devem tratar o obsoleto fec0::/10 como Global Unicast (seção 2.5.7): filtre pela alcançabilidade, não pelo tipo. O IPv6 Toolkit decodifica endereços, IPv4-mapped inclusive, e rotula o uso especial, mas não tem linhas como 3fff::/20, 2001::/23, 64:ff9b:1::/48, 5f00::/16 e 100:0:0:1::/64 e rotula pelo tipo, então 3fff::1 aparece como global unicast.

Propósito especial não quer dizer interno

Estar no registro não é veredito: cinco blocos IPv4 e nove IPv6 de propósito especial são Globally Reachable (registros IPv4 e IPv6 da IANA). Nem todo endereço fora das linhas é público: "While the assumption that a non-special-use address is reachable from the global Internet is generally safe, it is not always true", costuma valer, nem sempre (RFC 6598, seção 5.1). Uma virtual private cloud () da Amazon Web Services () pode usar espaço roteável publicamente (Amazon VPC User Guide), e muitas redes protegidas guardam endereços não privados (Jackson et al., seção 5.3).

Assim, um destino público ainda pode levar para dentro: o endereço está numa rede que você protege, o nome resolve para dentro na hora da conexão ou o servidor redireciona para dentro. Uma allow-list de destinos pretendidos resolve o primeiro caso (SSRF Prevention Cheat Sheet, caso 1); as próximas seções, os outros.

Normalizar, depois classificar

A gramática do Uniform Resource Identifier (URI) só aceita IPv4 em decimal pontuado, mas rotinas como inet_aton() aceitam muito mais (RFC 3986, seções 3.2.2 e 7.4): formas curtas como a.b.c ou um só número de 32 bits, com partes em octal (0 inicial) ou hexadecimal (0x inicial), e o exemplo do próprio manual transforma 0x7f.1 em 127.0.0.1 (inet(3)). O getaddrinfo() lê hosts numéricos do mesmo jeito (getaddrinfo(3)), o parser IPv4 do URL Standard lê hexadecimal e octal (URL Standard, seção 3.5), e quem recebe IPv6 aceita qualquer grafia legítima (RFC 5952, seção 4). O ipaddress do Python é estrito como o inet_pton() e recusa zeros à esquerda desde a 3.9.5 (documentação do ipaddress); o socket.inet_aton(), não. Sessão ilustrativa, Python 3.13.16 no 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'

Como ilustração, o URL Inspector segue a RFC 3986 e lê http://2130706433/ como nome registrado, enquanto o new URL() do Node devolve 127.0.0.1. A defesa da RFC 3986: "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", filtre o número, não o texto (RFC 3986, seção 7.4); a do cheat sheet: "Treat parser disagreement as a rejection", divergência é recusa (SSRF Prevention Cheat Sheet, caso 1).

O IPv6 também carrega o endereço: ::ffff:7f00:1 e 64:ff9b::7f00:1 contêm 127.0.0.1. Como isso pode enganar firewalls IPv4, "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", mesmo filtro para o IPv4 embutido (RFC 6052, seção 5.3). Os tradutores devem descartar pacotes que juntam o prefixo bem conhecido a um IPv4 não global (seção 3.1), regra que não cobre 64:ff9b:1::/48 (RFC 8215, seção 5): extraia o IPv4 e classifique-o como IPv4.

Resolva uma vez e conecte ao que verificou

Verificar um nome não é verificar uma conexão. No DNS rebinding, "the attacker rebinds the host name to the IP address of a target server that is inaccessible from the public Internet", o nome passa a apontar para dentro (Jackson et al., seção 1), e no servidor "A second, unchecked DNS lookup between validation and connection can bypass these checks", a segunda consulta anula o filtro (SSRF Prevention Cheat Sheet, caso 1). Resolva uma vez, verifique cada resposta A e e conecte a um endereço verificado, mantendo o hostname no header Host, no TLS Server Name Indication e na verificação do certificado, também em novas tentativas e fallbacks (mesmo cheat sheet). O curl fixa o endereço assim:

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

O --resolve faz o curl usar aquele endereço para aquele host e porta (página de manual do curl). Redirecionamentos, um 3xx com Location para dentro, são a outra porta: o cheat sheet conecta só ao endereço validado, sem redirecionamentos (SSRF Prevention Cheat Sheet, caso 2), e o curl só segue um com -L ou --location, enquanto --proto-redir limita os protocolos (página de manual do curl). Recuse redirecionamentos ou verifique cada hop.

Os endereços de metadados

Na nuvem, o prêmio é o serviço de metadados da instância (SSRF Prevention Cheat Sheet). O Instance Metadata Service () da AWS guarda as credenciais temporárias do papel da instância sem autenticação nem proteção criptográfica (AWS, Use instance metadata). A classe de ataque é um recurso que busca uma URL dada pelo usuário, como o download de uma imagem, apontado para o serviço (Alibaba Cloud, Instance metadata). Os endereços:

ProvedorEndereçosA requisição precisa levarFonte
AWS169.254.169.254, fd00:ec2::254um token de sessão, quando exigidoendereços, tokens
Google Cloudmetadata.google.internal, metadata.goog, 169.254.169.254, fd20:ce::254 para máquinas virtuais só IPv6Metadata-Flavor: Googlevisão geral, consulta
Azure169.254.169.254Metadata: truepágina do serviço
Alibaba Cloud100.100.100.200um token no Security Hardening Mode; nenhum por padrãopágina de metadados

A AWS diz "These IP addresses are link-local addresses and are valid only from the instance", link-local, só de dentro da instância (AWS, Access instance metadata), mas o registro põe fd00:ec2::254 e fd20:ce::254 no unique local fc00::/7 (registro IPv6 da IANA), e 100.100.100.200 está em 100.64.0.0/10: bloquear só 169.254.0.0/16 e fe80::/10 deixa passar três dos quatro.

Com o Instance Metadata Service Version 2 (IMDSv2), o token sai de um PUT e dura de um segundo a seis horas; quando ele é exigido, requisição sem token recebe 401 Unauthorized, PUT com X-Forwarded-For é recusado e a resposta ao PUT tem hop limit 1 por padrão (AWS, How IMDSv2 works). Google e Azure também recusam X-Forwarded-For (Google Cloud, consulta de metadados; Microsoft Learn, Azure IMDS). Header não é muralha, porque muitas falhas de SSRF deixam o atacante definir headers arbitrários (AWS Security Blog, 2019): o Google manda isolar em sandbox os processos que não devem chegar ao servidor (Google Cloud, visão geral de metadados), a Azure sugere regras locais de firewall (Microsoft Learn) e a AWS permite desligar o endpoint (AWS, Modify instance metadata options):

# Numa instância AWS: crie um token IMDSv2 e envie-o com a requisição
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/

# Exija tokens, desligue o endpoint, leia a configuração
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'

Como testar um classificador

O ipaddress do Python define is_private e is_global pelos registros da IANA, com exceções, e "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", no Shared Address Space as duas dão False (documentação do ipaddress). O endereço mapeado é julgado pelo IPv4 que carrega, extraído por ipv4_mapped; nada extrai o IPv4 de 64:ff9b::/96, e is_reserved não é a coluna Reserved-by-Protocol (mesma documentação). Saída ilustrativa, 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

Cada linha traz um endereço, suas seis flags e o IPv4 mapeado. 100.64.0.1 não é private nem global, então if ip.is_private: deny deixa passar o Shared Address Space e o endereço de metadados da Alibaba; 224.0.0.1, ff02::1 e 64:ff9b::7f00:1 (127.0.0.1 dentro) são global, então if not ip.is_global: deny, o teste do exemplo do cheat sheet (SSRF Prevention Cheat Sheet, caso 1), deixa passar multicast e loopback embutido em NAT64, a menos que is_multicast e o endereço embutido também sejam verificados.

A flag de uma biblioteca não é o registro. Em execuções ilustrativas do Python 3.13.16 e 3.14.2, is_global contradiz os registros de 2025-10-09 em quatro linhas, três alocadas em 2024 e 2025: 5f00::1, 100:0:0:1::1 e 192.88.99.2 saem como global sem que suas linhas o sejam, e 2001:1::3 sai como não global embora a sua o seja (registros IPv4 e IPv6 da IANA). As flags também mudaram três vezes só para IPv4-mapped, na 3.11, na 3.13 e na 3.14 (changelog do Python): fixe a versão e teste-a.

O Python playground roda o CPython 3.14.2 via Pyodide; numa execução ilustrativa, seu ipaddress bateu com o da 3.13.16, mas o getaddrinfo() falhou em 0x7f.1, então use-o só para ipaddress. Um plano de teste:

  1. Teste o primeiro e o último endereço de cada linha dos quatro registros; espere bloqueio onde Destination ou Globally Reachable for False e em todo o espaço multicast e reservado.
  2. Repita cada endereço bloqueado em todas as grafias que o cliente aceita: IPv4 pontuado, inteiro, hexadecimal, octal e curto; IPv4-mapped pontuado e hexadecimal; IPv6 sem compressão e em maiúsculas; com identificador de zona; formas em 64:ff9b::/96 e 64:ff9b:1::/48.
  3. Resolva um nome com várias respostas A e AAAA: cada resposta verificada, a conexão feita a uma delas.
  4. Sirva um redirecionamento para um endereço bloqueado.
  5. Rode tudo de novo a cada mudança da biblioteca ou do registro.

O SSRF URL classifier é uma verificação rápida de uma única URL: decodifica hosts IPv4 em decimal, octal, hexadecimal e forma curta, além da forma IPv4-mapped pontuada, e reconhece 169.254.169.254, 100.100.100.200, fd00:ec2::254, metadata.google.internal e metadata, mas nunca resolve nomes nem envia a requisição, e por isso os passos 3 e 4 ficam fora do seu alcance.

Para lembrar

  • Decida pelas colunas: bloqueie o destino cujo Destination ou Globally Reachable seja False; há blocos de propósito especial globalmente alcançáveis.
  • Multicast (224.0.0.0/4, ff00::/8) e o espaço IPv6 reservado pela IETF estão nos registros de espaço de endereçamento.
  • Trate 100.64.0.0/10 como interno: não é globalmente alcançável e abriga o endereço de metadados da Alibaba Cloud, 100.100.100.200.
  • Classifique o número a que o cliente se conecta, IPv4 dentro de IPv6 incluído, e recuse host que parsers leem de modos diferentes.
  • Resolva uma vez, verifique cada resposta, conecte ao endereço verificado e recuse redirecionamentos ou verifique cada hop.
  • Os endereços de metadados são 169.254.169.254, fd00:ec2::254, fd20:ce::254 e 100.100.100.200, e os dois IPv6 são unique local.
  • A flag de uma biblioteca não é o registro: as do Python divergem em 100.64.0.0/10, chamam multicast de global e contradizem quatro linhas.

Fontes