Skip to content

Preserve browser Origin with authenticated proxy forwarding - #1251

Merged
prk-Jr merged 57 commits into
mainfrom
fix/dev-proxy-rewrite-origin
Oct 30, 2026
Merged

prk-Jr merged 57 commits into
mainfrom
fix/dev-proxy-rewrite-origin

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Allow ts dev proxy to authenticate the browser-facing origin while preserving the browser's original Origin header. This lets trace Enable/End work when the proxy rewrites Host or sends plaintext to its upstream, without forging a trace-specific Origin.

Changes

  • Add --forwarder-secret-file. Validate a single secret of at least 32 ASCII graphic bytes before listening, redact diagnostics and mark the outgoing credential header sensitive. Credential injection requires a loopback listener.
  • On mapped intercepted requests, validate the actual browser Host or absolute-URI authority, retain its port, and stamp X-Forwarded-Host, X-Forwarded-Proto: https and X-TS-Forwarder-Auth after hop-by-hop sanitation. Conflicting or ambiguous authority is rejected; CONNECT fallback is never authenticated.
  • Remove incoming forwarding credentials from initial headers, trailer declarations and actual streaming trailer frames. Preserve benign trailers and streaming behavior.
  • Remove route-specific Origin rewriting. All Origin fields retain their original values on trace, publisher and vendor paths.
  • Document server provisioning and rollout. Add real wire tests and actual proxy-to-Axum trace/public-URL regressions alongside the existing proxy suite.

Dependency

Requires the server-side authenticated-forwarder support from #1107, now on main: optional [trusted_forwarder] configuration, secret-store resolution and shared public-origin handling.

Configure the server's authentication header as x-ts-forwarder-auth; its shared_secret is a secret-store key, while the CLI file contains the actual matching secret. Deploy the server configuration before using the flag.

Validation

All required local CI gates passed before the final merge with main: all eight lint gates, every adapter suite, parity, JS build/tests and formatting, native build-digest tests/lint, core documentation, native CLI suites and adapter builds including Fastly/Spin release artifacts. The CLI unit, proxy wire and actual proxy-to-server suites pass. Explicit TLS provider setup makes the new shared fixture dependency deterministic; the isolated TLS wire test and serial 111-test proxy unit suite also pass. The actual ts config validate command accepts the forwarder secret-store key reference. The combined regressions use the production Axum HTTP listener behind the real proxy, covering encrypted/plaintext upstreams, rewritten/preserved Host, a public non-default port, Enable/End cookies, rejected Origins/queries/duplicates, missing credentials, server opt-out and transport fallback. Publisher tests check unchanged Origin, stripped credentials, generated public URLs and HTTPS protocol-relative signing.

Independent reviewers checked the CLI implementation, found and verified a streaming trailer credential leak fix, and reviewed the real-server fixture. Server ingress and configured identify CORS are covered by #1107, now on main.

Closes #1250

Spec: docs/superpowers/specs/2026-06-22-ts-dev-proxy-design.md (updated here), plan: docs/superpowers/plans/2026-10-09-authenticated-forwarder.md

The new CodeQL alert #204 was independently reviewed and dismissed as a false positive. SHA-256 only creates temporary fixed-size bearer-token digests for comparison; neither digest is stored or exposed. Operator documentation requires cryptographically random token generation and distinguishes minimum length from entropy. This assessment follows CodeQL's guidance for the distinction between password storage and other hashing uses.

Standalone runtime verification: launched the actual trusted-server-axum server and ts dev proxy binaries, loading server configuration through the normal blob envelope and environment-backed secret store. All 56 live HTTPS requests passed the expected checks across plaintext/TLS upstreams with --rewrite-host on/off. Enable → separate active-state request → publisher document with active trace context → End → separate inactive-state request succeeded with unchanged Secure/HttpOnly/host-only/SameSite=Lax cookies. Public URLs retain browser HTTPS authority and port; protocol-relative signing uses HTTPS; publisher and custom Didomi routes preserve browser Origin and receive no forwarding credentials. Foreign hosts/Origins, duplicate Origin, action queries, nonempty bodies, missing/wrong credentials, and server opt-out reject without cookie mutation. Browser-to-proxy TLS uses the isolated generated CA; the self-signed local upstream TLS relay uses --insecure. An independent subagent audited the response and recorded upstream evidence. This does not establish physical mobile/browser UI or deployed-runtime acceptance.

The standalone run found a TLS-provider startup panic when workspace features enable both providers. The proxy now calls the existing idempotent provider initializer before TLS setup; a fresh-process regression requires a real PAC HTTP response. CI also exposed a redaction-test sentinel collision with a backtrace symbol; the distinct short invalid token retains the complete Report Debug redaction assertion and passes with short/full backtraces.

prk-Jr and others added 28 commits September 1, 2026 14:45
With --rewrite-host the proxy sends Host: TO but forwarded the browser's
Origin: https://FROM unchanged, so upstream endpoints that verify a
same-origin Origin against their own origin rejected proxied requests.
Replace a single same-origin Origin with the TO origin (scheme from
--upstream-plaintext, non-default port kept) so Origin and Host name the
same authority. Cross-site, null, plain-http and duplicated Origin values
pass through unchanged.
When Cookie field multiplicity is not preserved, any comma anywhere in the
header made all four trace cookie results unavailable. Google Identity's
JSON-valued g_state cookie contains commas and is set on every page load of
publishers that use One Tap, so tracing could never activate for those
visitors.

A comma is now ambiguous only when it could change a reserved result: inside
a reserved cookie's own segment, where it could truncate the value, or
directly before a reserved name, where it could hide an occurrence inside
another cookie's value. Commas inside unrelated values cannot do either. The
replacement-character rule is unchanged.

Update the four fixtures that relied on the old rule to use a comma that still
borders the session cookie, and align the spec and operator guide.
The trace page listed every retained field in one long column, so a reader
on a phone had to scroll through many screens of label/value pairs, raw
float timings and Unavailable rows to learn whether an ad slot filled.

Open the report with a What happened summary: a headline, a short reading
computed only from retained fields, count tiles and fill chips. A Needs
attention list links to empty, fill-unknown, incomplete or creative-failure
slots. Each GPT slot gets a card with its fill state, a proportional
response/render/viewable timing bar, its exact server-slot link and a
collapsed All details disclosure. Server auctions stay expanded; request,
cookie and coverage sections collapse. Unavailable fields fold behind
N fields unavailable, timings round to one decimal and capture times read
as UTC.

The summary never names an auction winner, counts only bidder-role bids so
mediator echoes are not double counted, names the outcome of auctions that
did not complete and applies no latency thresholds. Timing bars use
stylesheet flex-weight classes, not inline styles, so the strict
style-src 'self' CSP holds, and the exact milliseconds stay in text and in
the bar's accessible label. Light and dark themes follow the system setting.

Split the 680-line viewer into a pure, unit-tested summary model
(view-model.ts), DOM primitives (report-dom.ts), detail sections
(report-details.ts) and the page layout (report-view.ts). Rebuild the v1
trace assets and manifest, drop the setup shell's duplicated instruction,
and describe the page in spec section 6.3.
@prk-Jr prk-Jr self-assigned this Oct 7, 2026
Derive the first-party origin from the inbound Host so non-443 sessions
are rewritten too, and apply the rewrite only to Trusted Server's /_ts
endpoints, so publisher and integration requests keep the browser's real
Origin. Cover the behavior end to end through the proxy.
@prk-Jr
prk-Jr requested a review from aram356 October 9, 2026 10:12
aram356 and others added 3 commits October 11, 2026 12:43
Document authenticated forwarding and the current dev proxy limitation, pin the Spin runtime checksum, and keep composition tests compatible with ES2020.

Refresh the stale trace input digest after verifying rebuilt JS and CSS exactly match the frozen assets.
Resolve documentation overlaps while preserving authenticated forwarding, browser authority validation, and Origin handling.

Update the configuration guide and example for the available forwarding credential option.
aram356 added a commit that referenced this pull request Oct 12, 2026
Brings #1107 to b4a20f4, its first revision in this RC that carries
the trace implementation rather than only the spec.

Integration decisions:
- EdgeZero moves to 78439fd4 on spec/pre-dispatch-request-fidelity. It
  contains v0.0.9 (#1175) and the pre-dispatch ingress hooks #1107 and
  #1251 pin at 965d482f; no released tag carries both.
- scheduleInitialAdInit takes (bids, slots, auctionDiagnostics,
  traceAuctionTransport). One builder emits both payloads, so the RC's
  diagnostics-only scripts are unchanged and trace moves to the fourth
  argument. recordTrustedServerOpportunity likewise takes auctionFacts
  then traceIdentity; #1107's positional tests and the bootstrap
  fallback were shifted to match.
- The Fastly trace terminal path sends through the timed delivery
  function without Server-Timing, sandbox counters or access telemetry.
- build.rs embeds trace assets from #1214's private bundle directory.
  The viewer rebuilds byte-identical; the pinned source digest changes
  only because build-all.mjs gained --out-dir.
- The trace projection does not copy the RC's auction evidence fields,
  so the classification test marks them excluded.
- The retained diagnostics toolbar resyncs its filter select after
  selectRequest.
- Shim size guard raised to 55.5 KB for diagnostics plus trace.
aram356 added a commit that referenced this pull request Oct 12, 2026
aram356 added a commit that referenced this pull request Oct 12, 2026
Brings #1222 at 514a617.

Integration decisions:
- RequestInfo::from_request keeps #1251's source order (authenticated
  forwarder, immutable ingress, runtime origin, Host) and applies
  #1222's host syntax check to whichever host is selected. It returns
  BadRequest for a malformed or non-text selected host. Raw Forwarded
  and X-Forwarded-Host are never selected in the RC, so #1222's tests
  for those headers now assert they are ignored instead of rejected.
- RC-only callers (DataDome protection, Fastly compat, Spin, forwarder,
  orchestrator and publisher tests) handle the fallible signature.
  DataDome only reads RequestInfo behind an authenticated origin and
  keeps its request_host fallback.
- The first-party sign path prefers the authenticated forwarder scheme
  (#1251) and propagates #1222's host error.
- The GTM rewriter takes #1222's per-document GtmScriptState, which
  supersedes main's ScriptTextAccumulator use from #1179; state still
  lives in IntegrationDocumentState, and #1179's two-document residue
  tests remain.
Comment thread docs/guide/ts-dev-proxy.md Resolved
aram356 added a commit that referenced this pull request Oct 15, 2026
Brings in the squash-merged #1175 (a0b00c7), #1239 (d2f0d8c) and
#1226 (3e85161), replacing the RC's earlier branch revisions.

Conflict resolutions:
- EdgeZero stays pinned to 78439fd4. Main pins v0.0.9, which 78439fd4
  contains; #1107 and #1251 still need its pre-dispatch hooks, and no
  released tag carries them yet. The lockfile needed no changes.
- Fastly main.rs keeps #1107's ingress-capturing signatures, which
  already carry #1175's logical store selection.
- proxy.rs takes #1239's final redirect-limit passthrough test.
- AGENTS.md keeps the RC's gate list, which already includes main's
  wasm clock lint.
- getting-started takes main's final #1175/#1213 deploy wording.
- Integration-test config.rs and cloudflare.rs keep the RC's
  trace-aware envelope and readiness-probe helpers, which carry
  #1226's spawn_with_config_json.
@aram356
aram356 added this pull request to stack #1398 October 15, 2026 15:26
aram356 added a commit that referenced this pull request Oct 21, 2026
@aram356 aram356 modified the milestones: 202610, 202611 Oct 29, 2026
@prk-Jr
prk-Jr removed this pull request from stack #1398 October 30, 2026 05:16
@prk-Jr
prk-Jr changed the base branch from spec/mobile-ad-render-trace-endpoint to main October 30, 2026 13:53
@prk-Jr
prk-Jr merged commit 3d54bbd into main Oct 30, 2026
28 checks passed
@aram356
aram356 deleted the fix/dev-proxy-rewrite-origin branch October 30, 2026 18:51
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.

ts dev proxy --rewrite-host forwards a mismatched Origin, so same-origin POSTs are rejected

4 participants