From 1fc5389b5477d9dff88fcc482bba8289675c8ecd Mon Sep 17 00:00:00 2001 From: Robbie Byrd Date: Tue, 16 Jun 2026 02:34:10 +0000 Subject: [PATCH] fix(runtime-shim): emit absolute proxy URLs using proxyBase The server-side url-rewriter emits absolute proxy URLs when proxyBase is set (commit 0b5ce10) so cross-origin-embedded pages resolve assets against the canonical proxy host. The client runtime shim never matched: it emitted root-relative /web/... paths that the browser resolves against the embedding/browsing host, leaking the wrong hostname for every runtime-built URL (fetch, XHR, element src/href, document.write, MutationObserver). The earlier anti-double-wrap guard made it worse by stripping proxyBase off already-absolute proxy URLs, demoting correct absolute URLs to relative. Mirror buildProxyUrl: a proxify() helper prepends proxyBase to every emitted proxy path. Already-absolute proxy URLs are left untouched (idempotent, no demotion); root-relative ones are upgraded to absolute when proxyBase is set. When proxyBase is unset, output stays root-relative (backward-compatible direct-proxy default). Tests updated to assert the canonical-absolute contract. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/runtime-shim.ts | 33 +++++++++++++------ tests/lib/runtime-shim.test.ts | 60 ++++++++++++++++++++++------------ 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/src/lib/runtime-shim.ts b/src/lib/runtime-shim.ts index f65475b..3db230a 100644 --- a/src/lib/runtime-shim.ts +++ b/src/lib/runtime-shim.ts @@ -54,6 +54,17 @@ export const generateShimScript = ( return false; } + // Prepend the configured proxyBase so emitted URLs are ABSOLUTE, mirroring the + // server-side url-rewriter's buildProxyUrl. When pages are embedded cross-origin + // (e.g. inside beta.911realtime.org or reached via the box DNS dev.keepinghistory.org), + // a root-relative /web/... path resolves against the embedding/browsing host + // instead of the canonical proxy host. Absolute output pins every runtime-built + // URL to proxyBase. When proxyBase is unset, output stays root-relative (the + // backward-compatible direct-proxy default). + function proxify(path) { + return _base ? _base + path : path; + } + function rewrite(url, isLink) { if (!url || typeof url !== 'string') return url; var trimmed = url.trim(); @@ -62,16 +73,18 @@ export const generateShimScript = ( if (SKIP_RE.test(trimmed)) return url; var lock = _lock && !!isLink; // Wayback-wrapped (absolute or protocol-relative): unwrap to the embedded - // (ts, url) and emit a clean path-form URL the proxy understands. + // (ts, url) and emit a clean proxy URL the proxy understands. var wm = trimmed.match(WAYBACK_ABS_RE); - if (wm) return lock ? '/web/' + wm[2] : '/web/' + wm[1] + '/' + wm[2]; - // Absolute URL pointing at the proxy's own origin (emitted by the server-side - // url-rewriter when proxyBase is set): strip the base so the result is a - // root-relative /web/... path. Without this, the URL falls through to the - // new URL() branch below and gets double-wrapped as /web/im_//web/... - if (_base && trimmed.indexOf(_base + '/web/') === 0) return trimmed.slice(_base.length); - // Already proxied (root-relative path): idempotent pass-through. - if (trimmed.indexOf('/web/') === 0) return url; + if (wm) return proxify(lock ? '/web/' + wm[2] : '/web/' + wm[1] + '/' + wm[2]); + // Already an absolute proxy URL pointing at proxyBase (emitted by the + // server-side url-rewriter, or by a previous pass of this shim): leave it + // untouched. It is already in canonical absolute form — stripping it back to + // a root-relative path would re-introduce the cross-origin leak, and + // re-wrapping it would double-wrap as /web/im_//web/... + if (_base && trimmed.indexOf(_base + '/web/') === 0) return url; + // Root-relative proxy path: idempotent, but upgrade to absolute when + // proxyBase is set so it resolves against the proxy host, not the embedder. + if (trimmed.indexOf('/web/') === 0) return _base ? _base + trimmed : url; // Resolve relative URLs against the original page URL. var absolute; try { @@ -81,7 +94,7 @@ export const generateShimScript = ( } // Only rewrite http/https. if (absolute.indexOf('http://') !== 0 && absolute.indexOf('https://') !== 0) return url; - return lock ? '/web/' + absolute : '/web/' + _ts + 'im_/' + absolute; + return proxify(lock ? '/web/' + absolute : '/web/' + _ts + 'im_/' + absolute); } // --- window.fetch --- diff --git a/tests/lib/runtime-shim.test.ts b/tests/lib/runtime-shim.test.ts index 427f2ae..c3a2157 100644 --- a/tests/lib/runtime-shim.test.ts +++ b/tests/lib/runtime-shim.test.ts @@ -824,19 +824,19 @@ describe("rewrite() — path-form /web// input is pass-through (no doub }); }); -// ─── proxyBase: absolute proxy URL idempotency ─────────────────────────────── +// ─── proxyBase: runtime-built URLs are emitted ABSOLUTE ─────────────────────── // -// When the server-side url-rewriter has proxyBase set (e.g. https://dev.keepinghistory.org), -// it emits absolute hrefs like https://dev.keepinghistory.org/web//. -// Without this fix, the shim treats those as external URLs and double-wraps them: -// /web/im_/https://dev.keepinghistory.org/web// -// With the fix, the shim strips the proxyBase prefix to get the root-relative -// path form, which is the already-proxied idempotent guard. +// The server-side url-rewriter emits absolute hrefs (https://proxy.example.com/ +// web//) when proxyBase is set, so that pages embedded cross-origin (or +// reached via a secondary box DNS like dev.keepinghistory.org) resolve correctly. +// The shim MUST match: every URL it builds at runtime is also prefixed with +// proxyBase. A root-relative /web/... path would otherwise resolve against the +// embedding/browsing host, leaking the wrong hostname. const PROXY_BASE = "https://proxy.example.com"; -describe("rewrite() — proxyBase absolute URL is not double-wrapped", () => { - it("strips proxyBase prefix from an absolute proxy URL via window.fetch", async () => { +describe("rewrite() — proxyBase: runtime URLs are emitted absolute", () => { + it("prefixes proxyBase onto a relative URL via window.fetch (the core fix)", async () => { const captured: string[] = []; const realFetch = window.fetch; @@ -846,17 +846,39 @@ describe("rewrite() — proxyBase absolute URL is not double-wrapped", () => { }; installShim(TS, ORIG_URL, PROXY_BASE); - await window.fetch(`${PROXY_BASE}/web/${TS}/http://www.apple.com/`); + await window.fetch("/api/data.json"); + + installShim(); // restore default (no proxyBase) + window.fetch = realFetch; + + // Absolute, pinned to proxyBase — NOT a bare /web/... that would resolve + // against the embedding host. + expect(captured[0]).toBe(`${PROXY_BASE}/web/${TS}im_/http://www.example.com/api/data.json`); + }); + + it("keeps an already-absolute proxy URL untouched (no double-wrap, no demotion)", async () => { + const captured: string[] = []; + + const realFetch = window.fetch; + window.fetch = (input: RequestInfo | URL): Promise => { + captured.push(typeof input === "string" ? input : (input as Request).url); + return Promise.resolve({ ok: true } as Response); + }; + installShim(TS, ORIG_URL, PROXY_BASE); + + const already = `${PROXY_BASE}/web/${TS}/http://www.apple.com/`; + await window.fetch(already); installShim(); // restore default (no proxyBase) window.fetch = realFetch; - // Must be stripped to path form — NOT double-wrapped as /web/im_//web/... - expect(captured[0]).toBe(`/web/${TS}/http://www.apple.com/`); - expect(captured[0]).not.toContain(PROXY_BASE); + // Idempotent: already canonical absolute form — left exactly as-is. Must + // NOT be double-wrapped (/web/im_//web/...) nor demoted to a + // root-relative path (which would re-introduce the cross-origin leak). + expect(captured[0]).toBe(already); }); - it("strips proxyBase prefix with a timestamp modifier in the inner path", async () => { + it("upgrades a root-relative proxy path to absolute when proxyBase is set", async () => { const captured: string[] = []; const realFetch = window.fetch; @@ -866,16 +888,15 @@ describe("rewrite() — proxyBase absolute URL is not double-wrapped", () => { }; installShim(TS, ORIG_URL, PROXY_BASE); - await window.fetch(`${PROXY_BASE}/web/${INNER_TS}/http://cdn.example.com/img.gif`); + await window.fetch(`/web/${INNER_TS}/http://cdn.example.com/img.gif`); installShim(); window.fetch = realFetch; - expect(captured[0]).toBe(`/web/${INNER_TS}/http://cdn.example.com/img.gif`); - expect(captured[0]).not.toContain(PROXY_BASE); + expect(captured[0]).toBe(`${PROXY_BASE}/web/${INNER_TS}/http://cdn.example.com/img.gif`); }); - it("does not affect URLs on a different host when proxyBase is set", async () => { + it("wraps an external-host URL absolute under proxyBase", async () => { const captured: string[] = []; const realFetch = window.fetch; @@ -890,7 +911,6 @@ describe("rewrite() — proxyBase absolute URL is not double-wrapped", () => { installShim(); window.fetch = realFetch; - // External URL should still be wrapped, not stripped - expect(captured[0]).toBe(`/web/${TS}im_/http://other.com/page.html`); + expect(captured[0]).toBe(`${PROXY_BASE}/web/${TS}im_/http://other.com/page.html`); }); });