diff --git a/.changeset/healthy-ads-knock.md b/.changeset/healthy-ads-knock.md new file mode 100644 index 000000000..3cf325bd9 --- /dev/null +++ b/.changeset/healthy-ads-knock.md @@ -0,0 +1,5 @@ +--- +'@cloudflare/next-on-pages': patch +--- + +Make sure protocol relative URLs are not treated as actual relative URLs diff --git a/packages/next-on-pages/templates/_worker.js/utils/images.ts b/packages/next-on-pages/templates/_worker.js/utils/images.ts index 727b97ae8..aadbb13c6 100644 --- a/packages/next-on-pages/templates/_worker.js/utils/images.ts +++ b/packages/next-on-pages/templates/_worker.js/utils/images.ts @@ -66,7 +66,9 @@ export function getResizingProperties( return undefined; } - const isRelative = rawUrl.startsWith('/') || rawUrl.startsWith('%2F'); + const isProtocolRelative = rawUrl.startsWith('//'); + const isRelative = rawUrl.startsWith('/') && !isProtocolRelative; + if ( // Relative URL means same origin as deployment and is allowed. !isRelative && diff --git a/packages/next-on-pages/tests/templates/utils/images.test.ts b/packages/next-on-pages/tests/templates/utils/images.test.ts index c0d55fdf1..fe163058a 100644 --- a/packages/next-on-pages/tests/templates/utils/images.test.ts +++ b/packages/next-on-pages/tests/templates/utils/images.test.ts @@ -132,6 +132,21 @@ describe('getResizingProperties', () => { }); }); + ['/', '%2f', '%2F'].forEach(char => { + test(`image with valid request options succeeds (using '${char}'s)`, () => { + const baseValidUrl = `${baseUrl}${char}images${char}1.jpg`; + const url = new URL(`${baseValidUrl}&w=640`); + const req = new Request(url); + + const result = getResizingProperties(req, baseConfig); + expect(result).toEqual({ + isRelative: true, + imageUrl: new URL('https://localhost/images/1.jpg'), + options: { format: undefined, width: 640, quality: 75 }, + }); + }); + }); + test('svg image fails when config disallows svgs', () => { const url = new URL(`${baseValidUrl.replace('jpg', 'svg')}&w=640`); const req = new Request(url); @@ -167,6 +182,33 @@ describe('getResizingProperties', () => { }); }); + describe('protocol relative (potentially another origin) image', () => { + const protocolRelativePrefixes = ['%2F%2F', '//', '%2f%2f', '%2f/', '/%2f']; + + protocolRelativePrefixes.forEach(prefix => { + test(`image with valid request options succeeds (with ${prefix} prefix)`, () => { + const url = new URL( + `${baseUrl}${prefix}via.placeholder.com%2Fimage.jpg&w=640`, + ); + const req = new Request(url); + const result = getResizingProperties(req, baseConfig); + expect(result).toEqual({ + isRelative: false, + imageUrl: new URL('https://via.placeholder.com/image.jpg'), + options: { format: undefined, width: 640, quality: 75 }, + }); + }); + }); + + protocolRelativePrefixes.forEach(prefix => { + test(`image with disallowed domain fails (with "${prefix}" prefix)`, () => { + const url = new URL(`${baseUrl}${prefix}invalid.com%2Fimage.jpg&w=640`); + const req = new Request(url); + expect(getResizingProperties(req, baseConfig)).toEqual(undefined); + }); + }); + }); + describe('external image', () => { test('external image fails with disallowed domain', () => { const url = new URL(