rewrite the JSON-escaped form of the upstream origin too
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.
This commit is contained in:
@@ -71,6 +71,16 @@ describe('absolute URLs built from ROOT_URL', () => {
|
|||||||
expect(rewriteHtml('<a href="http://localhost:3000/explore">', BASE, ORIGINS)).toContain(`${BASE}/explore`);
|
expect(rewriteHtml('<a href="http://localhost:3000/explore">', 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', () => {
|
test('an unrelated host is left alone', () => {
|
||||||
const html = '<a href="https://github.com/some/repo">';
|
const html = '<a href="https://github.com/some/repo">';
|
||||||
expect(rewriteHtml(html, BASE, ORIGINS)).toBe(html);
|
expect(rewriteHtml(html, BASE, ORIGINS)).toBe(html);
|
||||||
|
|||||||
+15
-3
@@ -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.
|
* 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 {
|
export function rewriteAbsolute(html: string, base: string, origins: string[]): string {
|
||||||
|
const escapedBase = base.split('/').join('\\/');
|
||||||
let out = html;
|
let out = html;
|
||||||
for (const origin of origins.filter(Boolean)) {
|
for (const origin of origins.filter(Boolean)) {
|
||||||
// Escaped, because an origin is user-supplied config and a stray `.` or `?` would match too much.
|
// Regex-escaped, because an origin is user-supplied config and a stray `.` or `?` would match too much.
|
||||||
const escaped = origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const rx = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
out = out.replace(new RegExp(`${escaped}(?=/|"|'|\\s|$)`, 'g'), base);
|
|
||||||
|
// 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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user