From fe51058fe5b35b20b309f12655940bda2cb15293 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 25 May 2026 06:48:23 +0100 Subject: [PATCH] fix(docker): escape inline script injection to prevent XSS and add server-level integration tests - Add jsonForScriptTag() that escapes <, >, & after JSON.stringify to prevent breakout in inline config script - Sanitize rawBackendUrl in warning log to prevent log injection via newlines - Replace 5 duplicated-helper injection tests with 7 server-level HTTP integration tests that spawn the real docker-server.mjs with GITNEXUS_BACKEND_URL set - Add XSS-specific test: URL containing must produce exactly 1 ` + ? `` : ''; const contentTypes = { diff --git a/docker-server.test.mjs b/docker-server.test.mjs index f1118d5c3..60cdcfb58 100644 --- a/docker-server.test.mjs +++ b/docker-server.test.mjs @@ -106,60 +106,128 @@ it('returns 404 when dist/index.html is missing', async () => { assert.equal(res.status, 404); }); -// ── Config injection logic tests (inline, independent of server process) ───── +// -- Config injection: server-level integration tests --- -import { readFileSync, mkdirSync, writeFileSync } from 'node:fs'; +function spawnServerWithEnv(cwd, port, env) { + const proc = spawn(process.execPath, [serverScript], { + cwd, + env: { ...process.env, PORT: String(port), ...env }, + stdio: 'pipe', + }); + proc.on('error', (err) => { + throw err; + }); + return proc; +} -function isValidUrl(value) { +async function withInjectionServer(envOverrides, fn) { + const dir = await mkdtemp(join(tmpdir(), 'gitnexus-inject-')); + const distDir = join(dir, 'dist'); + const assetsDir = join(distDir, 'assets'); + await mkdir(assetsDir, { recursive: true }); + await writeFile( + join(distDir, 'index.html'), + 'app', + ); + await writeFile(join(assetsDir, 'style.abc.css'), 'body{}'); + + const port = await getFreePort(); + const proc = spawnServerWithEnv(dir, port, envOverrides); try { - const u = new URL(value); - return u.protocol === 'http:' || u.protocol === 'https:'; - } catch { - return false; + await waitForServer(port); + await fn(port); + } finally { + proc.kill(); + await rm(dir, { recursive: true, force: true }); } } -function makeInjectedHtml(envBackendUrl) { - const rawHtml = ''; - const backendUrl = envBackendUrl && isValidUrl(envBackendUrl) ? envBackendUrl : null; - const configScript = backendUrl - ? `` - : ''; - return configScript ? rawHtml.replace('', `${configScript}`) : rawHtml; -} - -it('injects __GITNEXUS_CONFIG__ when GITNEXUS_BACKEND_URL is a valid URL', () => { - const html = makeInjectedHtml('http://10.0.0.1:4747'); - assert.ok( - html.includes( - '', - ), - 'Expected config script to be injected into index.html', - ); +it('injects __GITNEXUS_CONFIG__ into / when GITNEXUS_BACKEND_URL is valid', async () => { + await withInjectionServer({ GITNEXUS_BACKEND_URL: 'http://10.0.0.1:4747' }, async (port) => { + const res = await rawGet(port, '/'); + assert.equal(res.status, 200); + assert.ok( + res.body.includes('window.__GITNEXUS_CONFIG__'), + 'Expected __GITNEXUS_CONFIG__ in response body', + ); + assert.ok(res.body.includes('http://10.0.0.1:4747'), 'Expected backend URL in response body'); + }); }); -it('does not inject when GITNEXUS_BACKEND_URL is not set', () => { - const raw = ''; - const html = makeInjectedHtml(null); - assert.equal(html, raw, 'Expected index.html to be unchanged when no env var is set'); +it('injects __GITNEXUS_CONFIG__ into SPA fallback routes', async () => { + await withInjectionServer({ GITNEXUS_BACKEND_URL: 'http://10.0.0.1:4747' }, async (port) => { + const res = await rawGet(port, '/some/deep/link'); + assert.equal(res.status, 200); + assert.ok( + res.body.includes('window.__GITNEXUS_CONFIG__'), + 'Expected __GITNEXUS_CONFIG__ in SPA fallback response', + ); + assert.ok( + res.body.includes('http://10.0.0.1:4747'), + 'Expected backend URL in SPA fallback response', + ); + }); }); -it('injects config script before ', () => { - const html = makeInjectedHtml('http://10.0.0.1:4747'); - const headCloseIdx = html.indexOf(''); - const scriptIdx = html.indexOf(' in GITNEXUS_BACKEND_URL to prevent XSS', async () => { + const xssUrl = 'http://example.com/?x='; + await withInjectionServer({ GITNEXUS_BACKEND_URL: xssUrl }, async (port) => { + const res = await rawGet(port, '/'); + assert.equal(res.status, 200); + + const scriptMatches = res.body.match(/