Skip to content

🛡️ feat(config): let a listener allowlist underscore-named fields - #324

Merged
dorianverlaine merged 1 commit into
mainfrom
codex/fix-295-underscore-allowlist
Oct 9, 2026
Merged

dorianverlaine merged 1 commit into
mainfrom
codex/fix-295-underscore-allowlist

Conversation

@dorianverlaine

Copy link
Copy Markdown
Owner

📝 What this changes

Implement Caddy's expected_underscore_headers server option: exact names and
trailing-star prefixes survive on the listeners that list them, the hyphenated
spelling of an allowlisted name is dropped, and a repeated allowlisted field
drops every occurrence. Without the option nothing changes — every underscore
name is still dropped before routing.

Closes #295.

🤔 Why

Underscore-named fields were dropped unconditionally, because a CGI or FastCGI
backend folds x_probe and x-probe onto the same variable and a client could
spoof the identity headers forward_auth copy_headers owns
(GHSA-f59h-q822-g45g). An origin that legitimately needs such a field had no way
to keep it; Caddy's expected_underscore_headers is that way, and this build
follows the current upstream semantics rather than the option's first draft in
the issue.

🔬 Evidence

  • pingclair/tests/integration/underscore_headers.rs (new, real binary, H1).
    With the proxy wiring removed the allowlist test fails against the origin's
    view — ["webhook-event", "x-probe"] instead of ["webhook_event", "x_probe"] — and passes with it.
  • scripts/h3-go/underscore_headers_test.go (new, real binary) asserts the
    same policy over H1, H2 and H3 in one test, including the hyphenated-alias,
    dot-suffix and duplicate rules; just h3-go-test passes in full.
  • pingclair-proxy/src/header_alias.rs unit tests pin exact, prefix, alias and
    duplicate semantics; pingclair-config tests cover entry validation and the
    refusal when two files disagree about the list.
  • just ci: 2,081 tests passed, 10 skipped; clippy, shear, repository lint,
    docs lint and benchmark smoke green.
  • git diff --check passes.

🔀 Both transports?

  • Verified on H1/H2 — the shared request_filter path, exercised by the
    Rust integration test (H1) and the Go suite (H1 and H2).
  • Verified on H3 — the H3 parser applies the same precomputed policy,
    exercised end to end by just h3-go-test.

📚 Documentation

CHANGELOG.md describes the new option, its default, and the alias and duplicate
rules.

🤖 Which AI helped?

Codex wrote the change and tests and ran the validation; Dorian reviewed the
approach (Caddy's whitelist semantics) before implementation.

Implement Caddy's `expected_underscore_headers` server option: exact names and
trailing-star prefixes survive on the listeners that list them, the matching
hyphenated spelling is dropped so the alias cannot reach the same CGI variable,
and a repeated allowlisted field drops every occurrence. Without the option the
default is unchanged — every underscore name is dropped before routing.

The allowlist is compiled once per listener and shared by the H1/H2 filter and
the H3 parser, so one configuration cannot route two ways (#269). Two files
disagreeing about the list are refused, like `client_ip_headers`.

Fixes #295

Co-authored-by: Codex <codex@openai.com>
@dorianverlaine

Copy link
Copy Markdown
Owner Author

@codex check

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@dorianverlaine
dorianverlaine merged commit 4970324 into main Oct 9, 2026
12 checks passed
@dorianverlaine
dorianverlaine deleted the codex/fix-295-underscore-allowlist branch October 9, 2026 07:14
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.

✨ server { allow_underscore_in_headers } has no spelling here, so the CGI-alias guard cannot be turned off

1 participant