Skip to content

docs: trust proxy spoke - #2535

Open
jonchurch wants to merge 2 commits into
expressjs:security-docsfrom
jonchurch:trust-proxy
Open

jonchurch wants to merge 2 commits into
expressjs:security-docsfrom
jonchurch:trust-proxy

Conversation

@jonchurch

Copy link
Copy Markdown
Member

This PR introduces the trust proxy spoke page for the security hub.

Suffice to say, I learned just how much I did not understand trust proxy while writing this!

Unrelated to this PR, but I do feel pretty strongly that we should rework this setting slightly in v6, specifically dropping the trust proxy: true option as I am convinced now that it is a footgun. We should also push users to setting an address range and not a hop count for this setting, which I realized very late into the process of writing this documentation, and these docs now reflect that better advice.

This PR also tweaks the two hub items I had previously for trust proxy, in order to better reflect what I have learned developers should ask themselves to determine if they are correctly using this setting, and if they are using it w/ a correctly configured proxy.

The structure I went for here is what I expect to do for the rest of the spokes:

  1. Introduce what Express does, and what goes wrong by default.
  2. One section per hub item, opening with the concrete problem and then the fix, under an anchorable heading
  3. What the setting or API can't do, and what the reader has to do elsewhere.
  4. Related links to the reference material, so the spoke explains the risk and the existing guide stays the reference.

Feedback on structure is highly relevant here!

@jonchurch
jonchurch requested a review from a team as a code owner October 16, 2026 19:46
@netlify

netlify Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for expressjscom-preview ready!

Name Link
🔨 Latest commit 5b8e989
🔍 Latest deploy log https://app.netlify.com/projects/expressjscom-preview/deploys/6ad27f10ac9bffc125381b87
😎 Deploy Preview https://deploy-preview-2535--expressjscom-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 97 (no change from production)
Accessibility: 100 (no change from production)
Best Practices: 100 (no change from production)
SEO: 100 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@crandmck

crandmck commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

For reviewers' convenience, here are the rendered pages, which aren't actually linked from anywhere in the deploy preview site:

@bjohansebas bjohansebas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a few minor comments; I’m not an expert on proxies.

```js
app.set('trust proxy', 1); // for an application behind a single proxy
```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the application trusts two proxy hops, it expects a path like Client → Proxy A → Proxy B → Application. However, if a client can also reach it through Client → Proxy B → Application, a forged value in X-Forwarded-For could be treated as the client’s IP address

It might be a good idea to add a note explaining this and recommending trusted proxy addresses or subnets when paths can have different lengths.

Suggested change
<Alert type="warning">
Only use a hop count when every possible path to your application has the expected number of trusted proxies. Blocking direct access alone is not sufficient: clients may still reach the application through a shorter proxy chain and supply a forged X-Forwarded-For value. If paths can have different lengths, configure trusted proxy addresses or subnets instead.
</Alert>


Many proxies do not overwrite `X-Forwarded-Host`, so your application should not derive its own host from the request at all. A forged value pollutes anything built from `req.hostname`, such as a password reset link that now points at the attacker's domain. Put your canonical host in configuration and use that instead.

When using a count as your `trust proxy` setting it is important to ensure your application is only reachable through the proxy and not directly from the internet. This is always a good idea behind a proxy, but applications that use a count are more exposed than those that use a range: a range checks the connecting address against your list before trusting any forwarded header, whereas a count trusts whichever address connected as the first hop, without checking what it is.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
When using a count as your `trust proxy` setting it is important to ensure your application is only reachable through the proxy and not directly from the internet. This is always a good idea behind a proxy, but applications that use a count are more exposed than those that use a range: a range checks the connecting address against your list before trusting any forwarded header, whereas a count trusts whichever address connected as the first hop, without checking what it is.
When using a hop count as your trust proxy setting, ensure clients cannot reach the application directly or through a shorter proxy chain than configured. Blocking direct access alone is not sufficient: for example, a client might bypass a CDN and connect directly to your load balancer. On that shorter path, Express may accept a client-supplied X-Forwarded-For value as req.ip. An address range or list checks the connecting address before trusting forwarded headers, whereas a count trusts hops based only on their position. Prefer trusted proxy addresses or subnets when paths can differ in length.


When you deploy an Express app behind a proxy, by default your application is no longer seeing the IP address in `req.ip` of the actual client who sent the request, as well as possibly seeing a different `req.hostname` or `req.protocol` than the original client used. This is a problem if your application is relying on those fields to make decisions like IP based rate limiting, including IP in access logs, or whether to set a session cookie marked `Secure`.

By default, Express sets `req.ip` from the socket the request arrived on, not from any header. When your application is directly reachable over the internet, that address is the client that made the request. When your application sits behind a proxy, such as a load balancer or a reverse proxy like Nginx, every request arrives from the proxy, so `req.ip` is the proxy's address, not the client's. The same is true of `req.hostname` and `req.protocol`: behind a proxy that terminates TLS, Express sees a plain HTTP request from the proxy.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
By default, Express sets `req.ip` from the socket the request arrived on, not from any header. When your application is directly reachable over the internet, that address is the client that made the request. When your application sits behind a proxy, such as a load balancer or a reverse proxy like Nginx, every request arrives from the proxy, so `req.ip` is the proxy's address, not the client's. The same is true of `req.hostname` and `req.protocol`: behind a proxy that terminates TLS, Express sees a plain HTTP request from the proxy.
By default, Express derives `req.ip` from `req.socket.remoteAddress`, which identifies the peer connected directly to the application. When your application sits behind a proxy, such as a load balancer or a reverse proxy like Nginx, this is normally the proxy's address rather than the original client's. `req.protocol` reflects whether the connection to Express uses TLS: if the proxy terminates TLS and forwards the request over HTTP, Express reports `http` even when the client used HTTPS. `req.hostname` comes from the `Host` header, so it can still reflect the original hostname if the proxy preserves that header.

@crandmck

crandmck commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

There's a fair amount to unpack here, and I'm going to have to come back to this with more detailed comments.

At a high level, I think we need to review and update the two other places that document trust proxy, linked in the "Related" section:

There is some duplication between these three docs, which is OK... IF there is a good reason for it:

  • The API reference doc should have exhaustive detail of the trust proxy options (does it?), and let's make sure all the examples illustrate good practices as outlined in this doc. Also add a link FROM the API ref section to this new "spoke" page.
  • What is the distinct purpose of "Express behind proxies"? Does it even make sense to keep that page, and if so, why? Let's just be very clear on its goal and then make sure its content speaks to that.

All three of these docs use CIDR notation in examples, with no explanation or references. While many developers may be familiar with it, we should at least say something like "We're assuming you're familiar with CIDR notation used for trust proxy option values" (with xref link). Also the API ref confusingly mixes IPv4 and IPv6 CIDR notation in the explanation of loopback, linklocal, and uniquelocal. This can easily be improved.

Finally, since the trust proxy setting is implemented using the proxy-addr package, for the sake of completeness, I think we should separately review https://github.com/jshttp/proxy-addr#readme to ensure there are no discrepancies with these docs. It's probably fine, but while we're here and this topic is fresh on our minds, it's worth at least a quick review IMO.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants