Affected versions

Symfony Framework Bundle and Symfony HTTP Foundation versions >=7.2, <7.4.23, >=8, <8.1.11 are affected by this security issue.

The issue has been fixed in Symfony Framework Bundle and Symfony HTTP Foundation 7.4.23, 8.1.11.

Description

framework.trusted_proxies: private_ranges, which the deployment documentation recommends, and the private_ranges and PRIVATE_SUBNETS keywords of Request::setTrustedProxies() resolved to IpUtils::PRIVATE_SUBNETS. That constant is the deny list of NoPrivateNetworkHttpClient: it lists the private and special-purpose prefixes a server must not connect to, and since the fix for CVE-2026-48736 it also covers the IPv6 prefixes that embed an IPv4 address: IPv4-mapped (::ffff:0:0/96), IPv4-compatible (::/96), 6to4 (2002::/16), Teredo (2001::/32) and NAT64 (64:ff9b::/96). Over-blocking is safe for a deny list. Used as the allow list of trusted proxies, the same entries trust remote clients.

A client whose connection arrives with such a source address was treated as a reverse proxy, and the X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto, X-Forwarded-Port and X-Forwarded-Prefix headers it sends were honoured:

1
2
3
4
5
6
7
8
9
10
11
Request::setTrustedProxies(['private_ranges'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PROTO);

$request = Request::create('/reset-password');
$request->server->set('REMOTE_ADDR', '::ffff:203.0.113.7');
$request->headers->set('X-Forwarded-For', '10.0.0.9');
$request->headers->set('X-Forwarded-Host', 'evil.example.com');
$request->headers->set('X-Forwarded-Proto', 'https');

$request->getClientIp(); // 10.0.0.9
$request->getHost();     // evil.example.com
$request->isSecure();    // true

Two kinds of deployments are exposed. A server listening on the IPv6 wildcard address without IPV6_V6ONLY reports every IPv4 client in the IPv4-mapped form, so every client is trusted. A server reachable over IPv6 trusts any client sourcing from 6to4 or Teredo space, which is globally routed, whatever its listener. A spoofed client address defeats IP allow lists, rate limiting and audit logs; a spoofed host poisons the URLs written into password reset and login link e-mails and into shared caches; a spoofed scheme satisfies requires_channel: https over plain HTTP.

The IPv4-mapped entry has been in the list since the private_ranges shortcut was introduced, in 7.1 for framework.trusted_proxies and in 7.2 for Request::setTrustedProxies(); the other prefixes joined it in 7.4.13 and 8.0.13.

Symfony 6.4 does not point any trusted proxy setting at IpUtils::PRIVATE_SUBNETS, so it is not affected by default. An application that passes that constant to Request::setTrustedProxies() itself is exposed the same way on any version. It should pass IpUtils::PRIVATE_RANGES instead, which 6.4.50 provides too.

Resolution

Trusted proxies get their own list. IpUtils::PRIVATE_RANGES holds the ranges reserved for private networks, in which a reverse proxy can live: loopback, RFC 1918, shared address space (RFC 6598), link-local and unique local addresses. It contains no prefix a remote client can present as its own address, and private_ranges and PRIVATE_SUBNETS now resolve to it. IpUtils::PRIVATE_SUBNETS is unchanged and remains the deny list of NoPrivateNetworkHttpClient.

IpUtils::checkIp() now judges an IPv4-mapped address as the IPv4 host it embeds, so a proxy that a dual-stack listener reports as ::ffff:10.0.0.5 is still trusted through 10.0.0.0/8, and an IPv4 range configured explicitly in trusted_proxies or in access_control.ips matches the clients that reach a dual-stack listener as well.

Two behaviour changes for operators: the documentation, benchmarking, "this network" and reserved ranges the old list covered (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 198.18.0.0/15, 0.0.0.0/8, 240.0.0.0/4, 2001:db8::/32, 2001:2::/48) are no longer trusted by the shortcut and have to be listed explicitly when a proxy lives there; and applications that pass IpUtils::PRIVATE_SUBNETS to Request::setTrustedProxies() themselves should pass IpUtils::PRIVATE_RANGES instead.

The patches for this issue are available here for branch 7.4 (and forward-ported to 8.1 and 8.2). IpUtils::PRIVATE_RANGES is available here for branch 6.4.

Credits

We would like to thank Ivan Tse for reporting the issue and Nicolas Grekas for providing the fix.