diff --git a/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts b/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts index 7d26a2bf0c8..4d4b33316ff 100644 --- a/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts +++ b/packages/app/src/cli/utilities/app/http-reverse-proxy.test.ts @@ -100,6 +100,29 @@ describe.sequential.each(each)('http-reverse-proxy for %s', (protocol) => { }) }) +describe('http-reverse-proxy unmatched paths', () => { + test('serves the invalid path error as non-sniffable plain text', async () => { + const abortController = new AbortController() + // No `default` rule, so every request falls through to the invalid path response. + const {server} = await getProxyingWebServer({'/path1': 'http://localhost:1'}, abortController.signal) + await new Promise((resolve) => server.listen(0, 'localhost', resolve)) + const port = (server.address() as net.AddressInfo).port + + try { + const response = await fetch(`http://localhost:${port}/`, { + agent: new http.Agent({keepAlive: false}), + }) + + expect(response.status).toBe(500) + expect(response.headers.get('content-type')).toBe('text/plain; charset=utf-8') + expect(response.headers.get('x-content-type-options')).toBe('nosniff') + } finally { + server.closeAllConnections() + await new Promise((resolve) => server.close(() => resolve())) + } + }) +}) + function getTestReverseProxy(protocol: 'http' | 'https') { return test.extend<{ setup: { diff --git a/packages/app/src/cli/utilities/app/http-reverse-proxy.ts b/packages/app/src/cli/utilities/app/http-reverse-proxy.ts index a0380b05995..088be7ea0ac 100644 --- a/packages/app/src/cli/utilities/app/http-reverse-proxy.ts +++ b/packages/app/src/cli/utilities/app/http-reverse-proxy.ts @@ -100,7 +100,10 @@ These are the allowed paths: ${outputToken.json(JSON.stringify(rules))} `) - res.statusCode = 500 + // The body echoes the requested path, so it must never be interpreted as HTML. + // Without an explicit content type browsers sniff the response, which would let a + // crafted path run script in the dev server's origin. + res.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8', 'X-Content-Type-Options': 'nosniff'}) res.end(`Invalid path ${req.url}`) } }