From 413b7ac0e95c9e37213ab973e14f9e340133ef0b Mon Sep 17 00:00:00 2001 From: officer Date: Wed, 19 Aug 2026 09:33:09 +0000 Subject: [PATCH] rewrite the JSON-escaped form of the upstream origin too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea's inline script carries `appUrl: 'https:\/\/gitea.example.com\/'`, and rewriteAbsolute only matched the literal `https://` form. So appUrl survived pointing at the real host while every visible link was rewritten correctly. The failure was quiet rather than obvious: the page rendered, and only the URLs Gitea's JS BUILDS from appUrl went wrong — cross-origin to the real host, answered with its login HTML. The heatmap reported "Unexpected token '<'" while parsing JSON, and the event worker failed the same way. Third instance of the same class today, after the asset paths and appSubUrl: a URL hiding in a shape the rewriter did not think to look for. All three were found by loading the page, none by the tests — and all three now have a test written against a live sample rather than an imagined one. --- sidecar/rewrite.test.ts | 10 ++++++++++ sidecar/rewrite.ts | 18 +++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/sidecar/rewrite.test.ts b/sidecar/rewrite.test.ts index 68f7610..a70d572 100644 --- a/sidecar/rewrite.test.ts +++ b/sidecar/rewrite.test.ts @@ -71,6 +71,16 @@ describe('absolute URLs built from ROOT_URL', () => { expect(rewriteHtml('', BASE, ORIGINS)).toContain(`${BASE}/explore`); }); + test('the JSON-ESCAPED form, which is how appUrl appears in Gitea\'s inline script', () => { + // `appUrl: 'https:\\/\\/gitea.officer.dev\\/'`. Missed at first, and the failure was quiet: the page + // rendered fine while every URL Gitea's JS built from appUrl went cross-origin to the real host and + // came back as its login HTML — so the heatmap reported "Unexpected token '<'" parsing JSON. + const real = String.raw`appUrl: 'https:\/\/gitea.officer.dev\/',`; + const out = rewriteHtml(real, BASE, ORIGINS); + expect(out).toContain(String.raw`'\/api\/gitea\/_web\/'`); + expect(out).not.toContain('gitea.officer.dev'); + }); + test('an unrelated host is left alone', () => { const html = ''; expect(rewriteHtml(html, BASE, ORIGINS)).toBe(html); diff --git a/sidecar/rewrite.ts b/sidecar/rewrite.ts index 3ebb75b..9e0b0b7 100644 --- a/sidecar/rewrite.ts +++ b/sidecar/rewrite.ts @@ -94,11 +94,23 @@ export function rewriteAppSubUrl(html: string, base: string): string { * configured with a public `ROOT_URL` emits THAT instead, so both are rewritten when they differ. */ export function rewriteAbsolute(html: string, base: string, origins: string[]): string { + const escapedBase = base.split('/').join('\\/'); let out = html; for (const origin of origins.filter(Boolean)) { - // Escaped, because an origin is user-supplied config and a stray `.` or `?` would match too much. - const escaped = origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - out = out.replace(new RegExp(`${escaped}(?=/|"|'|\\s|$)`, 'g'), base); + // Regex-escaped, because an origin is user-supplied config and a stray `.` or `?` would match too much. + const rx = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + // The plain form, in attributes and text. + out = out.replace(new RegExp(`${rx(origin)}(?=/|"|'|\\s|$)`, 'g'), base); + + // And the JSON-ESCAPED form, which is how it appears inside Gitea's inline script: + // appUrl: 'https:\/\/gitea.example.com\/' + // Missed at first, and the consequence was subtle rather than visible — the page rendered, but every + // URL Gitea's JS built from `appUrl` pointed at the real host, cross-origin. Those requests came back + // as Gitea's own login HTML, so the heatmap reported "Unexpected token '<'" while parsing JSON, and + // the event worker failed the same way. + const escapedOrigin = origin.split('/').join('\\/'); + out = out.replace(new RegExp(rx(escapedOrigin), 'g'), escapedBase); } return out; }