diff --git a/packages/qwik-city/middleware/node/http.ts b/packages/qwik-city/middleware/node/http.ts index cfc5586c381..148321bf976 100644 --- a/packages/qwik-city/middleware/node/http.ts +++ b/packages/qwik-city/middleware/node/http.ts @@ -20,7 +20,17 @@ function getOrigin(req: IncomingMessage) { export function getUrl(req: IncomingMessage) { const origin = ORIGIN ?? getOrigin(req); - return new URL((req as any).originalUrl || req.url || '/', origin); + return normalizeUrl((req as any).originalUrl || req.url || '/', origin); +} + +const DOUBLE_SLASH_REG = /\/\/|\\\\/g; + +export function normalizeUrl(url: string, base: string) { + // do not allow the url to have a relative protocol url + // which could bypass of CSRF protections + // for example: new URL("//attacker.com", "https://qwik.build.io") + // would return "https://attacker.com" when it should be "https://qwik.build.io/attacker.com" + return new URL(url.replace(DOUBLE_SLASH_REG, '/'), base); } export async function fromNodeHttp( diff --git a/packages/qwik-city/middleware/node/http.unit.ts b/packages/qwik-city/middleware/node/http.unit.ts new file mode 100644 index 00000000000..a9d82c283b0 --- /dev/null +++ b/packages/qwik-city/middleware/node/http.unit.ts @@ -0,0 +1,37 @@ +import { test } from 'uvu'; +import { equal } from 'uvu/assert'; +import { normalizeUrl } from './http'; + +[ + { + url: '/', + base: 'https://qwik.dev', + expect: 'https://qwik.dev/', + }, + { + url: '/attacker.com', + base: 'https://qwik.dev', + expect: 'https://qwik.dev/attacker.com', + }, + { + url: '//attacker.com', + base: 'https://qwik.dev', + expect: 'https://qwik.dev/attacker.com', + }, + { + url: '\\\\attacker.com', + base: 'https://qwik.dev', + expect: 'https://qwik.dev/attacker.com', + }, + { + url: '/some-path//attacker.com', + base: 'https://qwik.dev', + expect: 'https://qwik.dev/some-path/attacker.com', + }, +].forEach((t) => { + test(`normalizeUrl(${t.url}, ${t.base})`, () => { + equal(normalizeUrl(t.url, t.base).href, t.expect); + }); +}); + +test.run();