Express behind Cloudflare — trust proxy and CF-Connecting-IP
trust proxy is a hop count, and behind Cloudflare the number of hops is not obvious.
This page is about getting that count right, and about the second header everyone
reaches for once they have.
What Cloudflare sends
Section titled “What Cloudflare sends”Three things matter, all from Cloudflare’s own header reference:
X-Forwarded-For— if the request arrived with none, Cloudflare sets it to the same value asCF-Connecting-IP. If proxies already exist in the chain, Cloudflare appends to the list rather than replacing it.X-Forwarded-Proto— set to the protocol the client used, and if the client set a different value it is overwritten. You cannot forge this one through Cloudflare.CF-Connecting-IP— the client address as Cloudflare saw it.
The appending behaviour is the one that decides your hop count, because it means a client can put an entry to the left of Cloudflare’s.
Every value looks right until someone forges
Section titled “Every value looks right until someone forges”Measured on Express 5.2.1 under Node 25.9. First, an ordinary request through
Cloudflare, where the client sent no X-Forwarded-For of its own:
trust proxy |
req.ip |
|---|---|
unset (false) |
::ffff:127.0.0.1 — the connecting socket |
true |
203.0.113.7 |
1 |
203.0.113.7 |
2 |
203.0.113.7 |
Three different values, one correct answer. Nothing here tells you which is right.
Now the same application, same settings, with a client that sent
X-Forwarded-For: 1.2.3.4 before Cloudflare appended its own observation — so the
header arrives as 1.2.3.4, 203.0.113.7:
trust proxy |
req.ip |
|
|---|---|---|
unset (false) |
::ffff:127.0.0.1 |
proxy, not client |
true |
1.2.3.4 |
the forged value |
1 |
203.0.113.7 |
correct |
2 |
1.2.3.4 |
the forged value |
A hop count of 2 is exactly as exploitable as true here. It is also the value
people arrive at by reasoning “Cloudflare, then my load balancer, so two” — which is
right only if a second proxy of your own really does append a third entry. If
Cloudflare talks straight to your application, the count is 1, and 2 walks one
entry further left than Cloudflare wrote, into the part the client controls.
The important part is the pair of tables together: the misconfiguration is invisible in normal traffic. You cannot reach the right value by testing with a browser, because every wrong value returns the right answer until someone deliberately sends a header.
// Cloudflare is the only proxy in front of this app.app.set("trust proxy", 1);Count the hops that append to X-Forwarded-For, not the boxes the traffic passes
through. A TLS terminator or a container network that forwards without touching the
header does not add to the count.
trust proxyoptionBehind Cloudflare with no other proxy the answer is 1. Cloudflare appends to X-Forwarded-For rather than replacing it, so a count of 2 walks one entry further left than Cloudflare wrote — into the part the client controls. Reproduced: with a forged header, 2 and true both return the forged address.
- accepts
1|2|true|<ip or subnet>- default
false- set in
app.set()
CF-Connecting-IPheaderThe client IP as seen by Cloudflare. It is only evidence if the request actually arrived through Cloudflare — an origin reachable directly will accept this header from anyone. Cloudflare's documentation describes what it adds; it does not state that a client-supplied value is overwritten, so treat the header's authority as coming from the network path, not the header.
- set in
req.headers
# send an address that is not yours and see whether the app believes itcurl -s https://example.com/whoami -H 'X-Forwarded-For: 1.2.3.4'Expose req.ip on a temporary route, or read it out of your access log. If
1.2.3.4 appears anywhere, the count is too high. Run the same request without the
header to confirm the route works at all — a normal request proves nothing on its own,
which is the whole point of the tables above.
CF-Connecting-IP is only as good as the path
Section titled “CF-Connecting-IP is only as good as the path”Having found that req.ip is awkward, the usual next move is to skip it and read
req.headers['cf-connecting-ip'] instead. That is a plain string that arrived in a
request, and Express does nothing to it.
Cloudflare’s documentation describes what Cloudflare adds to a request. It does not
state that a client-supplied CF-Connecting-IP is overwritten — and for the related
True-Client-IP header it warns explicitly that in a stacked-CDN setup its value
“can be spoofed to any value” unless you set the header yourself.
The decisive question is not whether the header can be forged through Cloudflare. It
is whether anyone can reach your origin without going through Cloudflare at all. If
your origin answers requests from the open internet, an attacker skips the proxy,
writes whatever CF-Connecting-IP they like, and every control you built on it reads
their fiction. No Express setting can detect this, because from the application’s side
the request looks exactly like a real one.
That makes origin reachability part of this control rather than a separate concern. Cloudflare rates its own options: Cloudflare Tunnel and Authenticated Origin Pulls as “very secure”, and allowing only Cloudflare’s IP ranges at your firewall as “moderately secure”, since IP allowlisting is itself vulnerable to spoofing. Its guidance is that no single method is sufficient.
Until one of those is in place, CF-Connecting-IP and a correct trust proxy are
both claims rather than facts. The setting is the application’s half of the control;
the network path is the other half, and this page cannot close it for you.
Lowering trust proxy from true or 2 to 1 changes what every IP-based control
sees at once. Rate-limit buckets re-key, so counters reset and a user mid-limit gets a
clean slate. IP allow-lists start matching real client addresses instead of forged or
proxy ones — which is the point, and which will lock out anyone whose entry was added
while the wrong value was in place. Audit logs change meaning at the deploy boundary,
so keep the change in your log so later readers know why addresses shifted.
If you genuinely run a second proxy behind Cloudflare, the count is 2 and the table
above does not apply to you — confirm it with the forged-header check rather than by
counting infrastructure, since only the appending hops matter.
Raising the count is the dangerous direction and it fails silently, so treat any increase as a security change rather than a configuration tweak.
Related
Section titled “Related”- trust proxy — the setting itself, and the two values that are wrong everywhere
- Client IP spoofing via trust proxy — what this defeats when it is wrong
- Rate limiting — the control that depends most directly on this one
- Application vs server — the origin-reachability half of this page