Skip to content

4.1.0 hardening: exact proxy matching, deterministic header folding, config validation - #30

Merged
un33k merged 1 commit into
mainfrom
fix/deep-review-hardening
Sep 22, 2026
Merged

un33k merged 1 commit into
mainfrom
fix/deep-review-hardening

Conversation

@un33k

@un33k un33k commented Sep 22, 2026

Copy link
Copy Markdown
Owner

These are the fixes from a deep review of the modern engine. PR #29 was merged before they were pushed, so this is a fresh branch off main.

Security

  • Exact matching for complete-IP proxy entries. v3 matched these as prefixes, so proxy_list=["1.2.3.4"] also trusted 1.2.3.45, and that host could forge the client IP. Prefixes now match only on whole octets or groups: "10.1" matches 10.1.x.x but not 10.100.x.x. IPv6 entries ignore case and leading zeros. An entry ending in : still works as a prefix, as it did in 4.0.0.
  • Deterministic header folding. When more than one spelling of a header folds to the same name, the dash spelling always wins, whatever the dict order. A client-sent x_forwarded_for can no longer shadow the proxy's x-forwarded-for. Previously the result depended on dict order, even with a trusted proxy configured. If two dash spellings disagree, the header is treated as absent.
  • Quote-aware Forwarded parsing. A , or ; inside a quoted value no longer splits a hop, so ext="x,8.8.8.8" can't add a fake address or a fake proxy hop.

Correctness

  • Configuration errors now raise ValueError when the object is created:
    • proxy_list or precedence passed as a bare string. Previously each character became an entry.
    • A proxy_list entry that isn't an IP, such as "foo".
    • A proxy_count that is a bool, float or string.
  • The caller's precedence and proxy_list are copied, so later changes to those lists have no effect.
  • A meta that isn't a mapping raises TypeError. Non-string keys are skipped.
  • IPv4-mapped and NAT64 CIDR entries match the unwrapped IPv4 hops.
  • Address tiers:
    • 0.0.0.0/8 is never returned.
    • fec0::/10 (site-local) ranks as private; Python reports it as global.
    • 64:ff9b:1::/48 (local-use NAT64) ranks as private; Python reports it as reserved.
  • The dead is_valid_ip helper is removed. It was never exported.
  • proxy_count and proxy_list can still be set together and can differ, as in v3: the list sets the client position, and the count is a hop requirement. This is now documented and tested.

Tests

  • New tests/tests_modern_config.py covers config validation, every matcher form, count/list interplay, meta handling, Forwarded quoting and the new address classes.
  • The oracle matrix now includes an IPv6 exact entry, an IPv6 CIDR, a two-hop proxy list, a prefix without a dot, and a count/list mismatch. The oracle's proxy rule is written independently of the engine.
  • The modern engine now has 100% line and branch coverage.
  • I reintroduced each of the five bugs one at a time. The suite caught every one: exact-as-prefix, no boundary check, first-key-wins folding, trusting Python's is_global, and quote-blind splitting.

Verified locally

  • 182 tests pass on Python 3.11, 3.12, 3.13 and 3.14.
  • The v3 suite passes on the legacy engine, and the original v3 tests also pass on the modern engine.
  • ruff check . is clean.

No tag has been pushed, so nothing is published.

🚀 Generated with Dojo ⛩️

…r folding, config validation

- proxy_list: complete IPs match exactly (v3 prefix-matched, so 1.2.3.4
  trusted 1.2.3.45); prefixes match on octet/group boundaries; IPv6 entries
  case/zero-insensitive; mapped/NAT64 CIDRs match unwrapped IPv4 hops.
- Folded header keys: dash spelling wins regardless of dict order, so a
  client underscore header cannot shadow the proxy's; conflicts -> absent.
- Constructor rejects bare-string proxy_list/precedence, non-IP entries,
  bool/non-int proxy_count; copies caller lists. Non-mapping meta -> TypeError.
- RFC 7239 Forwarded parsing is quote-aware (no smuggled hops).
- Tiers: 0.0.0.0/8 rejected; fec0::/10 and 64:ff9b:1::/48 private.
- Remove dead is_valid_ip.
- Tests: new tests_modern_config.py, wider matrix configs (IPv6, two-hop,
  count/list mismatch); 100% line+branch coverage of modern engine.
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 98.768% (+1.7%) from 97.085% — fix/deep-review-hardening into main

@un33k
un33k merged commit 8e2fe10 into main Sep 22, 2026
18 checks passed
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.

2 participants