CakePHP 5.4.2, trustedProxies honors X-Forwarded-* from anyone
HttpsEnforcerMiddleware trustedProxies sets trustProxy true and never compares the list to REMOTE_ADDR. Unauth HTTP with X-Forwarded-Proto https skips the redirect. Labbed 5.4.2. No CVE yet.
- Name
- CakePHP 5.4.2, trustedProxies honors X-Forwarded-* from anyone
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- CakePHP
- Affected
- CakePHP 5.4.2 (887b641) HttpsEnforcerMiddleware with trustedProxies set. Default middleware has trustedProxies null and does not enable this. RateLimitMiddleware default remote_addr is not this leftover.
- Published
- 12 Oct 2026
- Updated
- 12 Oct 2026
- Tags
- 0-day, cakephp, trusted-proxies, cwe-441, cwe-346, https
The list is never compared to the peer
I am @abraxas_null. The proof of concept is on GitHub: abraxas/cakephp-trusted-proxies (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py, index.php, composer.json. Authorized lab only. It talks to loopback.
This is CakePHP 5.4.2 (887b641de9f7aba06071f98f17edcbc573cbcf63). No CVE yet. CWE-441 / CWE-346. 7.5 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N). Remote HTTPS redirect bypass / IP-host spoof.
HttpsEnforcerMiddleware with trustedProxies => ['10.0.0.1'] calls setTrustedProxies, which sets trustProxy = true and never compares the list to REMOTE_ADDR. After that, scheme(), host(), and clientIp() honor attacker X-Forwarded-Proto / X-Forwarded-For / X-Forwarded-Host / X-Real-IP.
Default middleware has trustedProxies null and does not enable this. The documented load-balancer setup is the exposed case. An unauthenticated HTTP client who is not the listed proxy can skip the HTTPS redirect and spoof client IP and Host.
An empty list still turns trust on. That is covered by a core test.
Why this leftover, on the same pin
I was already on 5.4.2 for the SMTP pair-only strip and the cookie-jar suffix. IP/Host/proxy is hunt six. 5.4.1 bakery already moved RateLimitMiddleware's default ipHeader from x-forwarded-for to remote_addr. That is a sibling that got the memo. setTrustedProxies did not.
I only wanted Critical or High. Local is in-scope. Medium stays on the floor unless it chains. Framework leftover: needs an app that enabled trustedProxies.
Pinning 5.4.2
Same tag. 887b641. Composer cakephp/cakephp 5.4.2 in PHP 8.3. Tiny app: / uses trustedProxies => ['10.0.0.1']; /control does not. Loopback HTTP at 127.0.0.1:19430.
Intake is security@cakephp.org.
The skip list is half the hunt
Same skip list as the SMTP pack. GHSA-vjqc, GHSA-fxf7, GHSA-2qh5 (incomplete, different leftover), GHSA-wpvj, GHSA-qh8m, GHSA-6g8q, 5.4.1 RateLimit ipHeader, 5.4.2 filename=. CSRF, View LFI, XXE, mass assignment, helper XSS, ungated HTTP_CLIENT_IP already gated. I did not re-file any of that.
LegalHackers clientIp() HTTP_CLIENT_IP without trustProxy is already gated. RateLimitMiddleware default remote_addr is not this bug. I wrote both down before I opened setTrustedProxies.
Ten surfaces, then the list
Default HttpsEnforcerMiddleware does not set trustedProxies. The leftover is the documented load-balancer list: the IPs are never compared to REMOTE_ADDR.
public function setTrustedProxies(array $proxies): void
{
$this->trustedProxies = $proxies;
$this->trustProxy = true;
$this->uri = $this->uri->withScheme($this->scheme());
}HttpsEnforcerMiddleware calls that whenever trustedProxies is non-null. The listed IPs are later used as an array_diff on the XFF chain, not as a check of the connecting peer.
Once trustProxy is on, scheme(), host(), port(), and clientIp() all honor attacker X-Forwarded-* / X-Real-IP / Client-IP.
Symfony isFromTrustedProxy() compares the peer. Cake does not.
What I almost filed instead
Treating default middleware as this leftover. trustedProxies => null does not enable trust. Control /control still 301s with the same forwarded headers.
Calling RateLimitMiddleware x-forwarded-for the same bug. 5.4.1 bakery already moved that default to remote_addr.
Claiming REMOTE_ADDR itself was spoofed. In the lab it is still the Docker bridge IP (192.168.208.1), not the listed 10.0.0.1. The leftover is that trust turns on anyway.
Inflating HTTPS bypass to RCE. Class is HTTPS redirect bypass / IP-host spoof.
A reverse shell. Theatre. The oracle is a 200 with scheme=https.
The lab
Composer cakephp/cakephp 5.4.2 in PHP 8.3. Tiny app: / uses trustedProxies => ['10.0.0.1']; /control does not. Loopback HTTP at 127.0.0.1:19430. how=app.
- GET
/no forwarded headers -> 301 https. - GET
/controlwith proto/for/host/real-ip -> 301 https. - GET
/withX-Forwarded-Proto: https-> 200, scheme https,trust_proxy=true,REMOTE_ADDRis not10.0.0.1. - GET
/with XFF + Host ->client_ip=203.0.113.50,host=spoof.invalid. - GET
/withX-Real-IP->client_ip=198.51.100.9.
Replay:
git clone https://github.com/abraxas/cakephp-trusted-proxies
cd cakephp-trusted-proxies/lab
./run.shBind it to loopback. The loopback client is lab/run.sh with a banner on it.
SUCCESS cakephp-trusted-proxies pin=5.4.2 how=app remote-addr=192.168.208.1 listed-proxy=10.0.0.1 proto-bypass=200 control-redirect=301 client-ip=203.0.113.50 host=spoof.invalid x-real-ip=198.51.100.9 CAKEPHP-TRUSTED-PROXIES-WITNESSWrong turns, in the order I made them
Default middleware. Null list does not enable this. The documented load-balancer setup does.
RateLimit ipHeader. Already patched. Different middleware.
REMOTE_ADDR spoof. The peer is still the bridge. Trust ignores that.
A reverse shell. Theatre. The oracle is a 200 with the witness in JSON.
What an attacker can do
Needs an app that enabled trustedProxies (typical reverse-proxy HTTPS enforce). Does not need to connect from a listed proxy IP.
| Privilege | Method / sink | Lab-proven or inferred |
|---|---|---|
| Skip HTTPS redirect / HSTS path | GET / HTTP with X-Forwarded-Proto: https |
Lab-proven. 200, scheme=https, witness in JSON |
Spoof clientIp() |
X-Forwarded-For: 203.0.113.50 |
Lab-proven. client_ip=203.0.113.50 |
Spoof host() |
X-Forwarded-Host: spoof.invalid |
Lab-proven. host=spoof.invalid |
Spoof via X-Real-IP |
X-Real-IP: 198.51.100.9 |
Lab-proven. client_ip=198.51.100.9 |
Bypass IP ACL / rate limit keyed on clientIp() |
Same spoof | Inferred. RateLimitMiddleware default remote_addr is not this bug |
| Negative control | Same forwarded headers on /control (no trustedProxies) |
Lab-proven. Still 301 to https |
Closed on this leftover: default trustedProxies => null; REMOTE_ADDR itself is still the Docker bridge IP in the lab. Not RCE. Not a cookie-jar bug.
The fix
Only honor forwarded headers when REMOTE_ADDR is in the trusted-proxy list (Symfony isFromTrustedProxy()). Empty list must not enable trust.
Until then, do not set trustedProxies unless the process is only reachable from those IPs. Binding the HTTP port to the load balancer is a workaround, not a fix.
I am not going to print an X-Forwarded-Proto you can paste at someone else's :80. The missing peer check is the useful part. If you own the app, run the proof of concept against loopback.
References
- Proof of concept: abraxas/cakephp-trusted-proxies · cakephp-trusted-proxies-Abraxas-Labs.py
- Lab:
lab/· Dockerfile · docker-compose.yml · run.sh · poc.py · index.php - @abraxas_null · github.com/abraxas · abraxaslabs.tech · abraxas.null@proton.me
- CWE-441 · CWE-346
- Tree: cakephp/cakephp 5.4.2 (
887b641de9f7aba06071f98f17edcbc573cbcf63) ·ServerRequest.php·HttpsEnforcerMiddleware.php - Same pin: cakephp-smtp-crlf · cakephp-cookie-suffix