diff --git a/packages/next/src/server/app-render/csrf-protection.test.ts b/packages/next/src/server/app-render/csrf-protection.test.ts index 84b2aebe94151..f7c515395db39 100644 --- a/packages/next/src/server/app-render/csrf-protection.test.ts +++ b/packages/next/src/server/app-render/csrf-protection.test.ts @@ -6,15 +6,22 @@ describe('isCsrfOriginAllowed', () => { expect(isCsrfOriginAllowed('www.vercel.com', ['www.vercel.com'])).toBe(true) }) - it('should return true when allowedOrigins contains originDomain with matching regex', () => { + it('should return true when allowedOrigins contains originDomain with matching pattern', () => { expect(isCsrfOriginAllowed('asdf.vercel.com', ['*.vercel.com'])).toBe(true) - expect(isCsrfOriginAllowed('asdf.jkl.vercel.com', ['*.vercel.com'])).toBe( + expect(isCsrfOriginAllowed('asdf.vercel.com', ['**.vercel.com'])).toBe(true) + expect(isCsrfOriginAllowed('asdf.jkl.vercel.com', ['**.vercel.com'])).toBe( true ) }) - it('should return false when allowedOrigins contains originDomain with non-matching regex', () => { + it('should return false when allowedOrigins contains originDomain with non-matching pattern', () => { expect(isCsrfOriginAllowed('asdf.vercel.com', ['*.vercel.app'])).toBe(false) + expect(isCsrfOriginAllowed('asdf.jkl.vercel.com', ['*.vercel.com'])).toBe( + false + ) + expect(isCsrfOriginAllowed('asdf.jkl.vercel.app', ['**.vercel.com'])).toBe( + false + ) }) it('should return false when allowedOrigins does not contain originDomain', () => { diff --git a/packages/next/src/server/app-render/csrf-protection.ts b/packages/next/src/server/app-render/csrf-protection.ts index 8d48774e8f1bb..8b842312c5f21 100644 --- a/packages/next/src/server/app-render/csrf-protection.ts +++ b/packages/next/src/server/app-render/csrf-protection.ts @@ -1,4 +1,36 @@ -import { isMatch } from 'next/dist/compiled/micromatch' +// micromatch is only available at node runtime, so it cannot be used here since the code path that calls this function +// can be run from edge. This is a simple implementation that safely achieves the required functionality. +// the goal is to match the functionality for remotePatterns as defined here - +// https://nextjs.org/docs/app/api-reference/components/image#remotepatterns +// TODO - retrofit micromatch to work in edge and use that instead +function matchWildcardDomain(domain: string, pattern: string) { + const domainParts = domain.split('.') + const patternParts = pattern.split('.') + + // Iterate through each part and compare them + for (let i = 0; i < patternParts.length; i++) { + if (patternParts[i] === '**') { + // If '**' is encountered, ensure remaining domain ends with remaining pattern + // e.g. **.y.z and a.b.c.y.z should match (b.c.y.z ends with y.z) + const remainingPattern = patternParts.slice(i + 1).join('.') + const remainingDomain = domainParts.slice(i + 1).join('.') + return remainingDomain.endsWith(remainingPattern) + } else if (patternParts[i] === '*') { + // If '*' is encountered, ensure remaining domain is equal to remaining pattern + // e.g. *.y.z and c.y.z should match (y.z is equal to y.z) + const remainingPattern = patternParts.slice(i + 1).join('.') + const remainingDomain = domainParts.slice(i + 1).join('.') + return remainingDomain === remainingPattern + } + + // If '*' is not encountered, compare the parts + if (patternParts[i] !== domainParts[i]) { + return false + } + } + + return true +} export const isCsrfOriginAllowed = ( originDomain: string, @@ -7,6 +39,7 @@ export const isCsrfOriginAllowed = ( return allowedOrigins.some( (allowedOrigin) => allowedOrigin && - (allowedOrigin === originDomain || isMatch(originDomain, allowedOrigin)) + (allowedOrigin === originDomain || + matchWildcardDomain(originDomain, allowedOrigin)) ) }