Repository navigation
Replies: 2 comments
|
The reproduction example relies heavily on custom code outside of a normal Laravel application, so moving this to Ideas. Also documentation already explains the proper usage: https://laravel.com/framework/docs/13.x/requests#configuring-trusted-proxies |
|
Thanks for the clarification. I agree that the standalone reproduction doesn't represent the normal Laravel application bootstrap. One thing that still seems worth clarifying is the trust boundary around the automatic Forge/Vapor detection: the current Would it make sense to document why the Forge/Vapor host-based detection is considered safe, or alternatively have the platform detection rely on a server-side signal rather than the incoming I'm mainly interested in understanding the intended security model here, rather than treating the standalone reproduction as proof of a vulnerability. |
Uh oh!
There was an error while loading. Please reload this page.
Laravel Version
12.22.0, 13.0.0
PHP Version
8.4
Database Driver & Version
No response
Description
Illuminate\Http\Middleware\TrustProxiesships in Laravel's default global middleware stack and decides whether the application should honourX-Forwarded-*headers. That decision is a trust boundary: forwarding headers are only meaningful once the request has been shown to originate from a known reverse proxy, which is normally established by matchingREMOTE_ADDR— the one request property an attacker cannot forge — against an operator-configured proxy list.When no trusted proxies have been configured, the middleware attempts to auto-detect Laravel's own hosting platforms and, on a match, trusts every proxy. Of the three auto-detection conditions, only
laravel_cloud()reads server-side state; the other two key on the request'sHostheader:handle()clears the trusted-proxy list at line 54 before this check runs.Request::host()delegates to Symfony'sgetHost(), which substitutesX-Forwarded-Hostonly when the request already arrives from a trusted proxy; with the list empty that condition is false by construction, so$request->host()returns the raw client-suppliedHostheader. The security decision is therefore taken on attacker-controlled input, and on a match the trusted-proxy list is set to0.0.0.0/0and::/0at line 122 — every host on the internet.Nothing in a default installation prevents the attacker from supplying that
Hostvalue. TheTrustHostsmiddleware, which would reject unrecognised hosts, is excluded from the stack byprotected $trustHosts = falseinFoundation/Configuration/Middleware.php:105, and the documentation states that Laravel responds to all requests regardless ofHostby default. Notrustedproxy.phpconfiguration file ships with the framework, so theconfig('trustedproxy.proxies')fallback at line 69 resolves tonulland the auto-detection branch is reached.Suggested Remediation
Remove the two
str_ends_with($request->host(), ...)clauses at lines 73-74. Platform auto-detection must derive from server-side state, exactly as the adjacentlaravel_cloud()call already does by reading$_ENV['LARAVEL_CLOUD']/$_SERVER['LARAVEL_CLOUD'](Support/helpers.php:233-237). Forge and Vapor should expose an equivalent provisioning-time environment variable, and the branch should key on that:Additionally, the documentation for trusted proxies does not describe it as a security control, while the
config('trustedproxy.proxies')fallback read at line 69 is not documented at all. Framing trusted-proxy configuration as a trust-boundary decision would make the consequences of leaving it unset visible to operators.Steps To Reproduce
composer.json:{ "require": { "laravel/framework": "^13.0" } }poc.phpalongside it with the following content. Each request is built with a fixed, honestREMOTE_ADDRof198.51.100.7and a forgedX-Forwarded-For: 1.2.3.4; only theHost:header varies:Host:header alone decides whether the forgedX-Forwarded-Foris honoured:The first line is the control: with an ordinary
Host:, no proxies are trusted andip()correctly returns the realREMOTE_ADDR. On lines two and three the request is byte-for-byte identical apart from theHost:value, yet the trusted-proxy list has become0.0.0.0/0+::/0,ip()now returns the attacker-chosen1.2.3.4, andsecure()has been spoofed totrueviaX-Forwarded-Proto. No server-side state was changed between the three calls —laravel_cloud()isfalsethroughout, and$proxiesis unset.All reactions