diff --git a/packages/next/src/server/image-optimizer.ts b/packages/next/src/server/image-optimizer.ts index 1a97536b9a6a0..87823065b6c8f 100644 --- a/packages/next/src/server/image-optimizer.ts +++ b/packages/next/src/server/image-optimizer.ts @@ -198,6 +198,12 @@ export class ImageOptimizerCache { return { errorMessage: '"url" parameter is too long' } } + if (url.startsWith('//')) { + return { + errorMessage: '"url" parameter cannot be a protocol-relative URL (//)', + } + } + let isAbsolute: boolean if (url.startsWith('/')) { diff --git a/test/integration/image-optimizer/app/pages/api/no-header.js b/test/integration/image-optimizer/app/pages/api/no-header.js new file mode 100644 index 0000000000000..287931027217f --- /dev/null +++ b/test/integration/image-optimizer/app/pages/api/no-header.js @@ -0,0 +1,5 @@ +export default function handler(_req, res) { + // Intentionally missing Content-Type header so that + // the fallback is not served when optimization fails + res.end('foo') +} diff --git a/test/integration/image-optimizer/test/util.ts b/test/integration/image-optimizer/test/util.ts index 015e04363f162..83344cc4fbc12 100644 --- a/test/integration/image-optimizer/test/util.ts +++ b/test/integration/image-optimizer/test/util.ts @@ -987,8 +987,17 @@ export function runTests(ctx: RunTestsCtx) { expect(await res.text()).toBe(`"url" parameter is too long`) }) + it('should fail when url is protocol relative', async () => { + const query = { url: `//example.com`, w: ctx.w, q: 1 } + const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {}) + expect(res.status).toBe(400) + expect(await res.text()).toBe( + `"url" parameter cannot be a protocol-relative URL (//)` + ) + }) + it('should fail when internal url is not an image', async () => { - const url = `//

not-an-image

` + const url = `/api/no-header` const query = { url, w: ctx.w, q: 39 } const opts = { headers: { accept: 'image/webp' } } const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts)