Skip to content

Commit 5a501d3

Browse files
authored
chore(next/image): improve imgopt api bypass detection for unsupported images (#74569)
Backport #73909 to 15.1.x
1 parent 4cbaaa1 commit 5a501d3

File tree

4 files changed

+20
-12
lines changed

4 files changed

+20
-12
lines changed

packages/next/src/server/image-optimizer.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ const JPEG = 'image/jpeg'
3737
const GIF = 'image/gif'
3838
const SVG = 'image/svg+xml'
3939
const ICO = 'image/x-icon'
40+
const ICNS = 'image/x-icns'
4041
const TIFF = 'image/tiff'
4142
const BMP = 'image/bmp'
4243
const CACHE_VERSION = 4
4344
const ANIMATABLE_TYPES = [WEBP, PNG, GIF]
44-
const VECTOR_TYPES = [SVG]
45+
const BYPASS_TYPES = [SVG, ICO, ICNS, BMP]
4546
const BLUR_IMG_SIZE = 8 // should match `next-image-loader`
4647
const BLUR_QUALITY = 70 // should match `next-image-loader`
4748

@@ -186,6 +187,9 @@ export function detectContentType(buffer: Buffer) {
186187
if ([0x00, 0x00, 0x01, 0x00].every((b, i) => buffer[i] === b)) {
187188
return ICO
188189
}
190+
if ([0x69, 0x63, 0x6e, 0x73].every((b, i) => buffer[i] === b)) {
191+
return ICNS
192+
}
189193
if ([0x49, 0x49, 0x2a, 0x00].every((b, i) => buffer[i] === b)) {
190194
return TIFF
191195
}
@@ -673,7 +677,10 @@ export async function imageOptimizer(
673677
}> {
674678
const { href, quality, width, mimeType } = paramsResult
675679
const { buffer: upstreamBuffer, etag: upstreamEtag } = imageUpstream
676-
const maxAge = getMaxAge(imageUpstream.cacheControl)
680+
const maxAge = Math.max(
681+
nextConfig.images.minimumCacheTTL,
682+
getMaxAge(imageUpstream.cacheControl)
683+
)
677684

678685
const upstreamType =
679686
detectContentType(upstreamBuffer) ||
@@ -708,10 +715,7 @@ export async function imageOptimizer(
708715
upstreamEtag,
709716
}
710717
}
711-
if (VECTOR_TYPES.includes(upstreamType)) {
712-
// We don't warn here because we already know that "dangerouslyAllowSVG"
713-
// was enabled above, therefore the user explicitly opted in.
714-
// If we add more VECTOR_TYPES besides SVG, perhaps we could warn for those.
718+
if (BYPASS_TYPES.includes(upstreamType)) {
715719
return {
716720
buffer: upstreamBuffer,
717721
contentType: upstreamType,
@@ -790,7 +794,7 @@ export async function imageOptimizer(
790794
return {
791795
buffer: optimizedBuffer,
792796
contentType,
793-
maxAge: Math.max(maxAge, nextConfig.images.minimumCacheTTL),
797+
maxAge,
794798
etag: getImageEtag(optimizedBuffer),
795799
upstreamEtag,
796800
}

test/integration/image-optimizer/test/util.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export function runTests(ctx: RunTestsCtx) {
241241
expect(res.status).toBe(200)
242242
expect(res.headers.get('content-type')).toContain('image/gif')
243243
expect(res.headers.get('Cache-Control')).toBe(
244-
`public, max-age=0, must-revalidate`
244+
`public, max-age=${isDev ? 0 : minimumCacheTTL}, must-revalidate`
245245
)
246246
expect(res.headers.get('Vary')).toBe('Accept')
247247
expect(res.headers.get('etag')).toBeTruthy()
@@ -258,7 +258,7 @@ export function runTests(ctx: RunTestsCtx) {
258258
expect(res.status).toBe(200)
259259
expect(res.headers.get('content-type')).toContain('image/png')
260260
expect(res.headers.get('Cache-Control')).toBe(
261-
`public, max-age=0, must-revalidate`
261+
`public, max-age=${isDev ? 0 : minimumCacheTTL}, must-revalidate`
262262
)
263263
expect(res.headers.get('Vary')).toBe('Accept')
264264
expect(res.headers.get('etag')).toBeTruthy()
@@ -275,7 +275,7 @@ export function runTests(ctx: RunTestsCtx) {
275275
expect(res.status).toBe(200)
276276
expect(res.headers.get('content-type')).toContain('image/png')
277277
expect(res.headers.get('Cache-Control')).toBe(
278-
`public, max-age=0, must-revalidate`
278+
`public, max-age=${isDev ? 0 : minimumCacheTTL}, must-revalidate`
279279
)
280280
expect(res.headers.get('Vary')).toBe('Accept')
281281
expect(res.headers.get('etag')).toBeTruthy()
@@ -292,7 +292,7 @@ export function runTests(ctx: RunTestsCtx) {
292292
expect(res.status).toBe(200)
293293
expect(res.headers.get('content-type')).toContain('image/webp')
294294
expect(res.headers.get('Cache-Control')).toBe(
295-
`public, max-age=0, must-revalidate`
295+
`public, max-age=${isDev ? 0 : minimumCacheTTL}, must-revalidate`
296296
)
297297
expect(res.headers.get('Vary')).toBe('Accept')
298298
expect(res.headers.get('etag')).toBeTruthy()
@@ -312,7 +312,7 @@ export function runTests(ctx: RunTestsCtx) {
312312
expect(res.headers.get('Content-Length')).toBe('603')
313313
expect(res.headers.get('Content-Type')).toContain('image/svg+xml')
314314
expect(res.headers.get('Cache-Control')).toBe(
315-
`public, max-age=0, must-revalidate`
315+
`public, max-age=${isDev ? 0 : minimumCacheTTL}, must-revalidate`
316316
)
317317
// SVG is compressible so will have accept-encoding set from
318318
// compression

test/unit/image-optimizer/detect-content-type.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ describe('detectContentType', () => {
3434
const buffer = await getImage('./images/test.ico')
3535
expect(detectContentType(buffer)).toBe('image/x-icon')
3636
})
37+
it('should return icns', async () => {
38+
const buffer = await getImage('./images/test.icns')
39+
expect(detectContentType(buffer)).toBe('image/x-icns')
40+
})
3741
it('should return tiff', async () => {
3842
const buffer = await getImage('./images/test.tiff')
3943
expect(detectContentType(buffer)).toBe('image/tiff')
90.8 KB
Binary file not shown.

0 commit comments

Comments
 (0)