| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
The tunnel proxy strips Cookie / Authorization inbound and Set-Cookie
outbound by design, so a tunnel exposes PUBLIC content only — no
bearer/DPoP access, and no cookie-session flows (interactive OIDC
login can never complete) through a tunnelled pod.
This adds the opt-in: `{ type: 'register', name, passthrough: true }`.
When the registering client opts in, the proxy forwards Cookie and
Authorization to the tunnel client and Set-Cookie back to the
visitor. Off by default; the ack echoes the applied mode so the
client knows what the relay actually did.
Security shape (documented in the protocol header):
- Opt-in is per-tunnel and owner-asserted — forwarding means
visitor credentials bound for the tunnelled service are handed to
the registered client, which is safe exactly when the registrant
owns that service (the normal my-pod-through-my-relay case).
- Strict `=== true` so a truthy-but-wrong value ('true', 1) can't
silently enable credential forwarding.
- Proxy-Authorization is ALWAYS stripped — it is addressed to the
relay, never to the tunnelled service.
- Set-Cookie array values survive JSON serialization and Fastify
emits one header per entry.
Known limitation, per the issue: passthrough alone doesn't make
path-based login flows fully work (/tunnel/<name>/ cookie-Path and
redirect rewriting — the reverse-proxy-at-a-subpath problem). It
delivers authenticated API access today and is the prerequisite for
the subdomain-routing endgame.
Tests (4 new, including pinning the previously-untested default):
default strips both ways, passthrough forwards all three headers
(multi-value Set-Cookie included), Proxy-Authorization never
forwarded, non-boolean opt-in rejected. 952/952 passing.
Closes #530.
There was a problem hiding this comment.
Adds an opt-in, per-tunnel “credential passthrough” mode to the tunnel relay so authenticated traffic can traverse a tunnel when explicitly requested at registration time (while preserving the existing default of stripping credentials for public-only tunneling).
Changes:
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/tunnel/index.js | Implements per-tunnel passthrough flag and conditional header forwarding/stripping, with updated protocol doc. |
| test/tunnel.test.js | Adds coverage for default credential stripping and opt-in passthrough behavior (including strict parsing and Proxy-Authorization handling). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Sorry, something went wrong.
| // headers.get('set-cookie') joins multiple values — assert both | ||
| // cookies survived without relying on getSetCookie() (Node 18.15+). | ||
| const setCookie = res.headers.get('set-cookie') || ''; | ||
| assert.ok(setCookie.includes('sess=abc'), `sess cookie must survive; got: ${setCookie}`); | ||
| assert.ok(setCookie.includes('csrf=xyz'), `csrf cookie must survive; got: ${setCookie}`); |
There was a problem hiding this comment.
Fixed in dc3a28c — feature-detect: when Headers#getSetCookie exists (Node ≥18.15), join its array and assert BOTH cookies; on older runtimes degrade to get('set-cookie') and assert only the first cookie, so the test is non-flaky on every Node the engines field admits (floor bump tracked in #541). Same approach as the #554 resolution, scoped to the assertion rather than a whole-test skip since the tunnel itself runs fine on older Node.
Sorry, something went wrong.
…feature-detect
Copilot: get('set-cookie') behaviour varies by undici version and may
expose only one value; Headers#getSetCookie() (Node ≥18.15) is the
supported multi-value API. Feature-detect: assert the full cookie
pair on the real API, degrade to a single-cookie assertion on older
runtimes (engines floor bump tracked in #541). Non-flaky on every
Node the engines field admits.
| assert.strictEqual(res.headers.get('set-cookie'), null, | ||
| 'set-cookie from the tunnel client must NOT reach the visitor by default'); |
There was a problem hiding this comment.
Fixed in 9824bc2 — feature-detect on the strip side too: when getSetCookie() exists, assert it returns an empty array; otherwise fall back to the get('set-cookie') === null check. Good catch that this one was sharper than pass-1's: a vacuous pass here would have weakened the security-default pin rather than just flaking. Both directions of the Set-Cookie assertions now use the supported API when present.
Sorry, something went wrong.
… available
Copilot: on some undici versions get('set-cookie') returns null even
when Set-Cookie headers ARE present (only readable via getSetCookie),
making the default-strip assertion vacuously pass — a false positive
that could mask a regression in the security default. Feature-detect:
assert getSetCookie() returns [] when the API exists, fall back to
the null check otherwise. Mirror of the pass-1 fix, applied to the
strip side.
| // Forward relevant headers. Hop-by-hop headers are always skipped; | ||
| // credentials (cookie / authorization) are skipped UNLESS the tunnel | ||
| // registered with passthrough (#530 — owner opted in to receive | ||
| // visitor credentials). Proxy-Authorization is always stripped: it | ||
| // is addressed to this relay, never to the tunnelled service. | ||
| const skipHeaders = new Set(['host', 'connection', 'upgrade', 'transfer-encoding', 'proxy-authorization']); |
There was a problem hiding this comment.
Real find — fixed in 7dfa236. Even in passthrough mode the proxy now strips the relay's own oidc cookies (_session*, _interaction*, matched at a ./_ boundary so a tunnelled service's unrelated _sessionsLeft-style cookie isn't caught) from the forwarded Cookie header. The visitor's cookies for the tunnelled service pass through; the relay's session cookies never reach the tunnel client, so there's nothing to replay against /idp/*. Chose tunnel-layer stripping over re-pathing the IdP cookies to /idp — it's defense-in-depth that holds regardless of cookie-path config or whether subdomain routing is added later, and it doesn't touch the OIDC flow's cookie semantics. New test asserts a mixed Cookie header forwards only the app cookie. 12/12.
Sorry, something went wrong.
| // Close old tunnel with same name from same user | ||
| if (existing) { | ||
| existing.socket.close(); | ||
| } |
| * → { type: "register", name: "myapp", passthrough?: true } | ||
| * ← { type: "registered", name: "myapp", url: "/tunnel/myapp/", passthrough: false } |
…rough mode Copilot found a real session-hijack vector. oidc-provider sets the relay's _session / _interaction cookies with path=/, so a browser attaches them to /tunnel/... requests too. With passthrough on, the proxy forwarded the FULL Cookie header — handing a visitor's RELAY session cookies to the tunnel client, which could replay them against the relay's /idp/* endpoints (session hijack / cross-app credential leak). The tunnel owner opted in to receive credentials for THEIR service, not the relay's own session. Fix: even in passthrough mode, strip the relay's own IdP cookies (_session*, _interaction* — matched at a / boundary so a tunnelled service's unrelated cookie like survives) from the forwarded Cookie header. The visitor's cookies bound for the tunnelled service pass through; the relay's do not. Defense-in-depth that holds regardless of cookie-path config or subdomain routing. Also: corrected the protocol-header ack example from to (the relay echoes the applied mode — Copilot's second comment). New test: visitor carries both an app cookie and the relay's _session/_session.sig/_interaction cookies; only the app cookie is forwarded. 12/12 tunnel, full suite green.
| function stripRelayCookies(cookieHeader) { | ||
| if (typeof cookieHeader !== 'string') return ''; | ||
| return cookieHeader | ||
| .split(';') | ||
| .map((s) => s.trim()) | ||
| .filter(Boolean) | ||
| .filter((pair) => { | ||
| const eq = pair.indexOf('='); | ||
| const name = (eq === -1 ? pair : pair.slice(0, eq)).trim(); | ||
| return !isRelayCookieName(name); | ||
| }) | ||
| .join('; '); | ||
| } |
There was a problem hiding this comment.
Fixed in aee9646 — stripRelayCookies now normalizes an array Cookie value (join with '; ') before filtering, so passthrough doesn't drop every cookie if Fastify surfaces string[]. Defensive in practice (Node folds duplicate Cookie headers into one string) but cheap and correct.
Sorry, something went wrong.
…eaders Copilot: header values can be string | string[] in Node/Fastify (folded duplicate Cookie headers). The helper returned '' for a non-string input, which would drop ALL cookies in passthrough mode if an array ever arrives. Normalize array → join with '; ' before filtering. Defensive (Node usually folds Cookie to a string) but cheap.
There was a problem hiding this comment.
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)src/tunnel/index.js:295
for (const [k, v] of Object.entries(res.headers)) {
if (!hopHeaders.has(k.toLowerCase())) {
reply.header(k, v);
}
}
Sorry, something went wrong.
Eight PRs merged since 0.0.206 — IdP hardening, protocol conformance, and the de-Googled-phone (#46) arc: IdP / auth - #558 passkey login degrades cleanly on stale WebViews / insecure contexts instead of crashing — drops a redundant browser crypto.randomUUID and guards both ceremonies on secure-context + WebAuthn (closes #556) - #554 IdP login-form error now survives the POST→redirect→GET cycle (rode in a non-persisted field that Interaction.save() dropped); failed sign-ins show the error instead of a silent re-render (closes #514) - #551 RFC 9207 'iss' authorization-response param normalized to match the discovery issuer, so strict OIDC clients (solid-oidc) complete sign-in (closes #524) Tunnel - #555 opt-in, per-tunnel credential passthrough (Cookie / Authorization / Set-Cookie) so authenticated access works through a tunnel; the relay's own IdP session cookies are isolated from the tunnel client (closes #530) Content negotiation / git - #553 HEAD now mirrors GET's negotiated Content-Type / Content-Length / Cache-Control for files (RFC 9110 §9.3.2 parity) (closes #552) - #550 git WAC preHandler 401/402/403 responses carry the git CORS headers, so browser git clients see the status, not a CORS error (closes #548) - #549 first HTTP-contract coverage for the git handler + fixes a DATA_ROOT test-pollution bug (closes #375) Docs / metadata - #547 README tagline + npm keywords surface the agentic positioning (closes #406)
| Back | FazBrowse Home | New Git URL |
Closes #530.
What
The tunnel proxy strips Cookie/Authorization inbound and Set-Cookie outbound by design — tunnels expose public content only, and no authenticated access (bearer/DPoP or cookie sessions, including interactive OIDC login) can work through one. This adds the issue's proposed opt-in:
With passthrough, the proxy forwards Cookie + Authorization to the tunnel client and Set-Cookie back to the visitor. Off by default; the ack echoes the mode the relay actually applied.
Security shape
The protocol doc header (the wire spec) documents the feature and the trust assertion ("safe exactly when the registrant owns the tunnelled service — the normal my-pod-through-my-relay case").
Known limitation (from the issue, on record)
Passthrough alone doesn't make path-based login flows fully work — Set-Cookie Path attributes and redirects assume root-hosting (/tunnel/<name>/ is the classic reverse-proxy-at-a-subpath problem). What ships today: authenticated bearer/DPoP API access through the tunnel, plus the prerequisite for the subdomain-routing endgame the issue sketches. Multi-value Set-Cookie survives as an array through JSON → Fastify emits one header per entry (tested).
Tests
4 new cases in test/tunnel.test.js (reusing the existing connectTunnel/waitMsg harness): default strips both directions, passthrough forwards all three headers (multi-cookie array included — asserted via headers.get('set-cookie'), deliberately not getSetCookie() per the Node-18 floor discussion on #554), Proxy-Authorization never forwarded, non-boolean opt-in rejected.
Full suite: 952/952 passing.
Refs