Skip to content

feat(proxy): trust the forwarding headers of a set of networks - #128

Merged
joamag merged 1 commit into
masterfrom
feat/proxy-trust-networks
Sep 13, 2026
Merged

joamag merged 1 commit into
masterfrom
feat/proxy-trust-networks

Conversation

@joamag

@joamag joamag commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

A reverse proxy that sits behind other proxies had only two choices for the forwarding headers: trust every origin (TRUST_ORIGIN=1) or none. Behind an HAProxy that relays raw TCP with the PROXY protocol, trusting every origin lets any client on the internet spoof X-Real-IP, while trusting none overwrites the real client address that an internal proxy (eg: a container on the Docker bridge) carefully passed on.

The change

A new TRUST_NETWORKS option (trust_networks in the constructor) takes a list of IPv4 addresses and CIDR networks, semicolon separated as every other list option:

TRUST_NETWORKS="172.17.0.0/16;10.0.0.1" python -m netius.extra.proxy_r

ProxyServer.is_trusted() decides the trust of a request: every peer is trusted under TRUST_ORIGIN, otherwise only an IPv4 peer inside one of the networks is. ReverseProxyServer.on_headers() now uses it in place of the global flag, so for a trusted peer X-Real-IP, X-Client-IP, X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Port and Forwarded are kept and passed on, and for any other peer they keep being replaced by what the proxy sees. Under the PROXY middleware the address that is checked is the one reported by the front-end, so a public client relayed by HAProxy is never trusted.

Two bugs found on the way

A network written with its host part set matched only some of its addresses. in_subnet_ip4 compared the bits of the address against the unmasked subnet, so a network written as the address of an interface, the way ip addr prints it, gave an answer that depended on the host bits:

>>> netius.common.in_subnet_ip4("172.17.5.3", "172.17.0.1/16")
True
>>> netius.common.in_subnet_ip4("172.17.5.2", "172.17.0.1/16")
False

The subnet is now masked before the comparison. This also affected the existing ALLOWED option.

A repeated forwarding header broke the request. The parser stores a repeated header as a sequence, so a trusted peer sending two X-Forwarded-For lines made on_headers raise AttributeError: 'list' object has no attribute 'split', and a repeated X-Forwarded-Proto ended up inside the canonical URL. An empty X-Real-IP was passed on as an empty client address. The values are now read through _prx_header, the same helper the rest of the proxy uses: the forwarded for values are joined (the first being the client), the last definition prevails for the others, and an empty value falls back to what the proxy sees. This was already reachable under TRUST_ORIGIN.

Tests

  • test_in_subnet_ip4_host covers a subnet written with its host part set, at both edges of the range
  • test_is_trusted covers the edges of a network, exact addresses, IPv6 and IPv4 mapped addresses, an invalid octet, an interface style network and TRUST_ORIGIN
  • test_on_serve and test_on_serve_env cover the option being given on creation and read from the environment
  • The on_headers cases cover a trusted peer, repeated headers, empty headers and an untrusted peer that tries to spoof them

Both bugs were reproduced by a failing test before being fixed. The complete suite passes (2021 passed, 16 skipped), black --check and stubtest are clean, and the new and changed code is at 100% coverage (27/27 statements, 18/18 branches).


Note

Medium Risk
Changes client IP derivation and spoofing rules for reverse proxy deployments; misconfigured TRUST_NETWORKS could trust forged headers or drop legitimate upstream forwarding data.

Overview
Adds TRUST_NETWORKS so the reverse proxy can trust X-Real-IP, X-Forwarded-For, and related headers only from listed IPv4 addresses/CIDRs—not from every client (TRUST_ORIGIN) or none.

ProxyServer.is_trusted() treats a peer as trusted when TRUST_ORIGIN is on or the connection’s IPv4 address matches trust_networks (via env/constructor). ReverseProxyServer uses that instead of the global flag for reading forwarding headers, stripping spoofed Forwarded from untrusted peers, and passing through values from trusted front proxies.

in_subnet_ip4 now masks the subnet prefix so CIDRs written with a host address (e.g. 172.17.0.1/16) match the whole network—also relevant for ALLOWED.

Forwarding header handling goes through _prx_header: repeated values no longer crash or corrupt URLs (join X-Forwarded-For, last value wins elsewhere), and empty values fall back to the peer address.

Reviewed by Cursor Bugbot for commit a19137f. Bugbot is set up for automated code reviews on this repo. Configure here.

- A peer in TRUST_NETWORKS has its X-Real-IP and X-Forwarded-* headers kept
- A repeated or empty forwarding header no longer breaks the client address
- A network written with its host part set now matches every address of it
Copilot AI lite review requested due to automatic review settings September 13, 2026 14:13
@joamag joamag self-assigned this Sep 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-13T14:21:44.529701Z a19137f Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 7b828346-85f1-4e50-887c-b81f889106d0

📥 Commits

Reviewing files that changed from the base of the PR and between 0c9c44d and a19137f.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • doc/configuration.md
  • src/netius/common/util.py
  • src/netius/extra/proxy_r.py
  • src/netius/servers/proxy.py
  • src/netius/servers/proxy.pyi
  • src/netius/test/common/util.py
  • src/netius/test/extra/proxy_r.py
  • src/netius/test/servers/proxy.py
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@joamag joamag added enhancement New feature or request p-medium Medium priority issue feature-request 💡 Request the addition of a feature and removed feature-request 💡 Request the addition of a feature labels Sep 13, 2026
@joamag
joamag requested a balanced review from Copilot September 13, 2026 14:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@joamag

joamag commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@joamag

joamag commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

@joamag joamag left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LGTM

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a19137f. Configure here.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: a19137f29d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@joamag
joamag merged commit 838a34d into master Sep 13, 2026
33 of 34 checks passed
@joamag
joamag deleted the feat/proxy-trust-networks branch September 13, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request p-medium Medium priority issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants