Affected versions

Symfony HTTP Kernel versions <5.4.56, >=6, <6.4.50, >=7, <7.4.23, >=8, <8.1.11 are affected by this security issue.

The issue has been fixed in Symfony HTTP Kernel 5.4.56, 6.4.50, 7.4.23, 8.1.11.

Description

When a page renders an inline fragment, with render() in Twig or with an ESI tag rendered inline because no gateway cache is used, SubRequestHandler adds 127.0.0.1 to the trusted proxies while the sub-request runs, with the trusted header set of the application, so that the internal sub-request is trusted. RouterListener::onKernelFinishRequest() runs inside that window: when the sub-request finishes, it rebuilds the router's RequestContext from the main request with RequestContext::fromRequest(). If the main request itself came from 127.0.0.1, its X-Forwarded-* and Forwarded headers are then trusted, although the application never declared 127.0.0.1 a trusted proxy.

From that point on, every absolute URL the router generates for the rest of the main request takes its host, scheme and port from headers sent by the client: url() in Twig, UrlGeneratorInterface::ABSOLUTE_URL, links in a page that a shared cache stores, or in an e-mail sent after the page was rendered.

This affects applications reached over HTTP from 127.0.0.1 without that address being a trusted proxy, for instance behind a TLS terminator or a reverse proxy running on the same host in front of an application server, behind a local HTTP cache, or behind a sidecar proxy. When framework.trusted_proxies is not configured, which is the default, every forwarded header is trusted during that window, X-Forwarded-Host included, and a web server or proxy that only accepts known hosts does not help, since the host comes from that header. Applications served through php-fpm, mod_php, or a proxy running on another host are not affected, since REMOTE_ADDR is not 127.0.0.1 there. When framework.trusted_hosts is configured, a forwarded host outside of the list is rejected, but the scheme and the port can still be changed.

Resolution

RouterListener now saves the router context of the parent request when a sub-request starts, and restores it when the sub-request finishes, instead of computing it again from the parent request while the trusted proxies of the sub-request still apply to it. URLs generated after an inline fragment are built from the same context as before it.

Code that reads the main request while the sub-request itself runs, such as Request::getClientIp() in a listener of that sub-request, still sees 127.0.0.1 trusted. Applications running behind a proxy on 127.0.0.1 should have that proxy set the forwarded headers itself instead of passing the client's along, and configure framework.trusted_hosts.

The patches for this issue are available here for branch 5.4 (and forward-ported to 6.4, 7.4, 8.1 and 8.2).

Credits

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