Skip to content

fix(proxy): strip unsafe HTTP headers before owner-bridge forward#1036

Open
ozpool wants to merge 2 commits into
Soju06:mainfrom
ozpool:fix/bridge-unsafe-headers-1016
Open

fix(proxy): strip unsafe HTTP headers before owner-bridge forward#1036
ozpool wants to merge 2 commits into
Soju06:mainfrom
ozpool:fix/bridge-unsafe-headers-1016

Conversation

@ozpool

@ozpool ozpool commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

build_owner_forward_headers copied the full downstream header map and only manually removed host and content-length. Hop-by-hop and HTTP-only headers — accept, accept-encoding, connection, content-type, cookie — were forwarded into the internal bridge POST and from there flowed through to the upstream Responses WebSocket handshake on the owner node.

Type of change

  • fix: — bug fix (no behavior change beyond the bug)

Linked issue: Closes #1016

OpenSpec

  • Not applicable — bug fix that matches the existing spec

Changes

  • Import filter_inbound_headers into http_bridge_forwarding.py
  • Add _BRIDGE_UNSAFE_HEADER_NAMES constant listing hop-by-hop and HTTP framing headers (accept, accept-encoding, connection, content-type, cookie, keep-alive, te, trailer, transfer-encoding, upgrade) that must not be forwarded to an upstream WebSocket path
  • Replace dict(headers) + manual pops in build_owner_forward_headers with filter_inbound_headers (strips authorization, host, content-length, x-forwarded-*, cf-*) followed by a second pass over _BRIDGE_UNSAFE_HEADER_NAMES
  • Add two regression tests: one verifying hop-by-hop headers are dropped, one verifying authorization/host/content-length are dropped while safe headers pass through

Test plan

uv run pytest tests/unit/test_http_bridge_forwarding.py -q
# 15 passed

uv run pytest tests/unit/ -q
# 2691 passed, 39 skipped

Checklist

  • Title is in Conventional Commits format (<type>(<scope>)?: <subject>).
  • Linked the related issue / discussion above.
  • Added or updated tests covering the change.
  • Ran uv run pytest tests/unit/ locally — all pass.
  • CHANGELOG is not edited by hand (release-please handles it).

build_owner_forward_headers forwarded the full downstream header map,
which could carry hop-by-hop and HTTP-only headers (accept, accept-encoding,
connection, content-type, cookie) into the internal bridge POST and from
there into the upstream Responses WebSocket handshake on the owner node.

Apply filter_inbound_headers first (removes authorization, host,
content-length, x-forwarded-*, cf-* headers) then additionally drop
_BRIDGE_UNSAFE_HEADER_NAMES (hop-by-hop and HTTP framing headers that
must not flow to a WebSocket connection).

Regression tests added to test_http_bridge_forwarding.py.

Fixes Soju06#1016
@Soju06

Soju06 commented Jun 17, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3ac797d9f9

ℹ️ 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".

forwarded = dict(headers)
forwarded.pop("host", None)
forwarded.pop("content-length", None)
filtered = filter_inbound_headers(headers)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve auth for internal bridge validation

In deployments with api_key_auth_enabled=True, forwarded owner requests need the client Authorization header on the internal POST because internal_bridge_responses() calls _validate_internal_bridge_api_key(request) before stripping headers for the upstream call. _forward_http_bridge_request_to_owner() deliberately re-adds that header via _headers_with_authorization(...), but this new filter_inbound_headers() call removes it again, so remote owner forwarding now reaches /internal/bridge/responses without auth and is rejected before the signed request can be streamed. Preserve the auth header for the internal bridge hop and strip it only before upstream forwarding.

Useful? React with 👍 / 👎.

forwarded.pop("host", None)
forwarded.pop("content-length", None)
filtered = filter_inbound_headers(headers)
forwarded = {key: value for key, value in filtered.items() if key.lower() not in _BRIDGE_UNSAFE_HEADER_NAMES}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drop headers named by the Connection header

When a downstream request uses Connection: X-Foo with an accompanying X-Foo header, this code removes only Connection and the fixed unsafe names, leaving X-Foo in the internal bridge request. Before this change the owner-side websocket header builder could see Connection and drop token-nominated hop-by-hop headers; after this point it no longer can, so those per-connection headers are forwarded upstream despite being marked hop-by-hop. Capture the Connection tokens before deleting the header and exclude those names as well.

Useful? React with 👍 / 👎.

@Soju06 Soju06 added the 🤖 codex: needs work [@codex review] raised an issue label Jun 17, 2026
The owner instance re-validates the client API key from the Authorization
header (_validate_internal_bridge_api_key) before swapping in its own
upstream access token, so routing build_owner_forward_headers through
filter_inbound_headers broke forwarded bridge requests whenever
api_key_auth_enabled is set: the header was stripped before the owner
could validate it.

Re-add Authorization after filtering, and also drop any header named by
the inbound Connection header to honor the hop-by-hop contract.
@ozpool

ozpool commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the catch on both, addressed in 8ffb546:

  • P1: build_owner_forward_headers now preserves the client Authorization header. The owner instance re-validates the client API key from it in _validate_internal_bridge_api_key before swapping in its own upstream token, so routing through filter_inbound_headers (which strips it) broke forwarded requests whenever api_key_auth_enabled is set. Added a regression test asserting it survives the forward.
  • P2: now also drops any header named by the inbound Connection token list, on top of the fixed unsafe set. Added a test covering a custom Connection-named header.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@ozpool

ozpool commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Friendly ping on this one — it's been sitting green since the fix went in. Codex's last review ran against 3ac797d (before the fix); the header-preservation change is in 8ffb546, so that feedback is stale now. Since the connector only triggers for your account, could you re-run @codex review or take a look when you get a chance? ty/pytest (unit + PostgreSQL) are passing.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

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

Labels

🤖 codex: needs work [@codex review] raised an issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: HTTP bridge forwards unsafe WebSocket headers and can break hard-affinity 1011 retries

2 participants