diff --git a/packages/next/shared/lib/match-remote-pattern.ts b/packages/next/shared/lib/match-remote-pattern.ts index 4db09ee4451e0..d132ee3aa2a38 100644 --- a/packages/next/shared/lib/match-remote-pattern.ts +++ b/packages/next/shared/lib/match-remote-pattern.ts @@ -28,10 +28,15 @@ export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean { } break } - if (patternParts[i] === '*') { - // Single asterisk means "match this part" so we can - // continue to the next part of the loop - continue + const patternPart = patternParts[i] || '' + const actualPart = actualParts[i] || '' + if (patternPart.includes('*') && !patternPart.includes('**')) { + // Single asterisk means "zero or more matches" similar to regex + // so we can test for a match and continue to the next part of the loop + // TODO: Don't use regex since its not safe + if (new RegExp(patternPart.replaceAll('*', '.*')).test(actualPart)) { + continue + } } if (patternParts[i] !== actualParts[i]) { return false @@ -59,10 +64,15 @@ export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean { } break } - if (patternParts[i] === '*') { - // Single asterisk means "match this subdomain" so we can - // continue to the next part of the loop - continue + const patternPart = patternParts[i] || '' + const actualPart = actualParts[i] || '' + if (patternPart.includes('*') && !patternPart.includes('**')) { + // Single asterisk means "zero or more matches" similar to regex + // so we can test for a match and continue to the next part of the loop + // TODO: Don't use regex since its not safe + if (new RegExp(patternPart.replaceAll('*', '.*')).test(actualPart)) { + continue + } } if (patternParts[i] !== actualParts[i]) { return false diff --git a/test/unit/image-optimizer/match-remote-pattern.test.ts b/test/unit/image-optimizer/match-remote-pattern.test.ts index cf45055077061..7191114c8406e 100644 --- a/test/unit/image-optimizer/match-remote-pattern.test.ts +++ b/test/unit/image-optimizer/match-remote-pattern.test.ts @@ -102,7 +102,7 @@ describe('matchRemotePattern', () => { expect(m(p, new URL('ftp://example.com/path/to/file'))).toBe(false) }) - it('should match hostname pattern with single asterisk', () => { + it('should match hostname pattern with single asterisk by itself', () => { const p = { hostname: 'avatars.*.example.com' } as const expect(m(p, new URL('https://com'))).toBe(false) expect(m(p, new URL('https://example.com'))).toBe(false) @@ -115,6 +115,54 @@ describe('matchRemotePattern', () => { expect(m(p, new URL('https://more.avatars.iad1.example.com'))).toBe(false) }) + it('should match hostname pattern with single asterisk at beginning', () => { + const p = { hostname: 'avatars.*1.example.com' } as const + expect(m(p, new URL('https://com'))).toBe(false) + expect(m(p, new URL('https://example.com'))).toBe(false) + expect(m(p, new URL('https://sub.example.com'))).toBe(false) + expect(m(p, new URL('https://example.com.uk'))).toBe(false) + expect(m(p, new URL('https://sub.example.com.uk'))).toBe(false) + expect(m(p, new URL('https://avatars.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.sfo1.example.com'))).toBe(true) + expect(m(p, new URL('https://avatars.iad1.example.com'))).toBe(true) + expect(m(p, new URL('https://more.avatars.iad1.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.sfo2.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.iad2.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.1.example.com'))).toBe(true) + }) + + it('should match hostname pattern with single asterisk in middle', () => { + const p = { hostname: 'avatars.*a*.example.com' } as const + expect(m(p, new URL('https://com'))).toBe(false) + expect(m(p, new URL('https://example.com'))).toBe(false) + expect(m(p, new URL('https://sub.example.com'))).toBe(false) + expect(m(p, new URL('https://example.com.uk'))).toBe(false) + expect(m(p, new URL('https://sub.example.com.uk'))).toBe(false) + expect(m(p, new URL('https://avatars.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.sfo1.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.iad1.example.com'))).toBe(true) + expect(m(p, new URL('https://more.avatars.iad1.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.sfo2.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.iad2.example.com'))).toBe(true) + expect(m(p, new URL('https://avatars.a.example.com'))).toBe(true) + }) + + it('should match hostname pattern with single asterisk at end', () => { + const p = { hostname: 'avatars.ia*.example.com' } as const + expect(m(p, new URL('https://com'))).toBe(false) + expect(m(p, new URL('https://example.com'))).toBe(false) + expect(m(p, new URL('https://sub.example.com'))).toBe(false) + expect(m(p, new URL('https://example.com.uk'))).toBe(false) + expect(m(p, new URL('https://sub.example.com.uk'))).toBe(false) + expect(m(p, new URL('https://avatars.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.sfo1.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.iad1.example.com'))).toBe(true) + expect(m(p, new URL('https://more.avatars.iad1.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.sfo2.example.com'))).toBe(false) + expect(m(p, new URL('https://avatars.iad2.example.com'))).toBe(true) + expect(m(p, new URL('https://avatars.ia.example.com'))).toBe(true) + }) + it('should match hostname pattern with double asterisk', () => { const p = { hostname: '**.example.com' } as const expect(m(p, new URL('https://com'))).toBe(false) @@ -129,7 +177,7 @@ describe('matchRemotePattern', () => { expect(m(p, new URL('https://more.avatars.iad1.example.com'))).toBe(true) }) - it('should match pathname pattern with single asterisk', () => { + it('should match pathname pattern with single asterisk by itself', () => { const p = { hostname: 'example.com', pathname: '/act123/*/pic.jpg', @@ -150,6 +198,79 @@ describe('matchRemotePattern', () => { expect(m(p, new URL('https://example.com/team/pic.jpg'))).toBe(false) }) + it('should match pathname pattern with single asterisk at the beginning', () => { + const p = { + hostname: 'example.com', + pathname: '/act123/*4/pic.jpg', + } as const + expect(m(p, new URL('https://com'))).toBe(false) + expect(m(p, new URL('https://example.com'))).toBe(false) + expect(m(p, new URL('https://sub.example.com'))).toBe(false) + expect(m(p, new URL('https://example.com.uk'))).toBe(false) + expect(m(p, new URL('https://example.com/act123'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4/pic'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4/picsjpg'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4/pic.jpg'))).toBe(true) + expect(m(p, new URL('https://example.com/act123/usr5/pic.jpg'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/team4/pic.jpg'))).toBe(true) + expect(m(p, new URL('https://example.com/act456/team5/pic.jpg'))).toBe( + false + ) + expect(m(p, new URL('https://example.com/team/pic.jpg'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/4/pic.jpg'))).toBe(true) + }) + + it('should match pathname pattern with single asterisk in the middle', () => { + const p = { + hostname: 'example.com', + pathname: '/act123/*sr*/pic.jpg', + } as const + expect(m(p, new URL('https://com'))).toBe(false) + expect(m(p, new URL('https://example.com'))).toBe(false) + expect(m(p, new URL('https://sub.example.com'))).toBe(false) + expect(m(p, new URL('https://example.com.uk'))).toBe(false) + expect(m(p, new URL('https://example.com/act123'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4/pic'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4/picsjpg'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4/pic.jpg'))).toBe(true) + expect(m(p, new URL('https://example.com/act123/usr5/pic.jpg'))).toBe(true) + expect(m(p, new URL('https://example.com/act123/team4/pic.jpg'))).toBe( + false + ) + expect(m(p, new URL('https://example.com/act456/team5/pic.jpg'))).toBe( + false + ) + expect(m(p, new URL('https://example.com/team/pic.jpg'))).toBe(false) + expect(m(p, new URL('https://example.com/act456/sr/pic.jpg'))).toBe(true) + }) + + it('should match pathname pattern with single asterisk at the end', () => { + const p = { + hostname: 'example.com', + pathname: '/act123/usr*/pic.jpg', + } as const + expect(m(p, new URL('https://com'))).toBe(false) + expect(m(p, new URL('https://example.com'))).toBe(false) + expect(m(p, new URL('https://sub.example.com'))).toBe(false) + expect(m(p, new URL('https://example.com.uk'))).toBe(false) + expect(m(p, new URL('https://example.com/act123'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4/pic'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4/picsjpg'))).toBe(false) + expect(m(p, new URL('https://example.com/act123/usr4/pic.jpg'))).toBe(true) + expect(m(p, new URL('https://example.com/act123/usr5/pic.jpg'))).toBe(true) + expect(m(p, new URL('https://example.com/act456/usr/pic.jpg'))).toBe(true) + expect(m(p, new URL('https://example.com/act123/team4/pic.jpg'))).toBe( + false + ) + expect(m(p, new URL('https://example.com/act456/team5/pic.jpg'))).toBe( + false + ) + expect(m(p, new URL('https://example.com/team/pic.jpg'))).toBe(false) + }) + it('should match pathname pattern with double asterisk', () => { const p = { hostname: 'example.com', pathname: '/act123/**' } as const expect(m(p, new URL('https://com'))).toBe(false)