Skip to content

Commit 96c35b7

Browse files
Merge branch 'canary' into revert/swc-css-bump
2 parents 24ce007 + 8a55612 commit 96c35b7

File tree

12 files changed

+208
-69
lines changed

12 files changed

+208
-69
lines changed

docs/api-reference/next/image.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ description: Enable Image Optimization with the built-in Image component.
1616

1717
| Version | Changes |
1818
| --------- | ------------------------------------------------------------------------------------------------- |
19-
| `v12.0.9` | `lazyRoot` prop added |
19+
| `v12.1.0` | `dangerouslyAllowSVG` and `contentSecurityPolicy` configuration added. |
20+
| `v12.0.9` | `lazyRoot` prop added. |
2021
| `v12.0.0` | `formats` configuration added.<br/>AVIF support added.<br/>Wrapper `<div>` changed to `<span>`. |
2122
| `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. |
2223
| `v11.0.0` | `src` prop support for static import.<br/>`placeholder` prop added.<br/>`blurDataURL` prop added. |
@@ -439,6 +440,21 @@ module.exports = {
439440
}
440441
```
441442

443+
### Dangerously Allow SVG
444+
445+
The default [loader](#loader) does not optimize SVG images for a few reasons. First, SVG is a vector format meaning it can be resized losslessly. Second, SVG has many of the same features as HTML/CSS, which can lead to vulnerabilities without proper [Content Security Policy (CSP) headers](/docs/advanced-features/security-headers.md).
446+
447+
If you need to serve SVG images with the default Image Optimization API, you can set `dangerouslyAllowSVG` and `contentSecurityPolicy` inside your `next.config.js`:
448+
449+
```js
450+
module.exports = {
451+
images: {
452+
dangerouslyAllowSVG: true,
453+
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
454+
},
455+
}
456+
```
457+
442458
## Related
443459

444460
For an overview of the Image component features and usage guidelines, see:

errors/invalid-images-config.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ module.exports = {
2727
minimumCacheTTL: 60,
2828
// ordered list of acceptable optimized image formats (mime types)
2929
formats: ['image/webp'],
30+
// enable dangerous use of SVG images
31+
dangerouslyAllowSVG: false,
32+
// set the Content-Security-Policy header
33+
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
3034
},
3135
}
3236
```

packages/next/client/image.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,10 @@ export default function Image({
385385
isLazy = false
386386
}
387387

388+
if (src.endsWith('.svg') && !config.dangerouslyAllowSVG) {
389+
unoptimized = true
390+
}
391+
388392
if (process.env.NODE_ENV !== 'production') {
389393
if (!src) {
390394
throw new Error(

packages/next/server/config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,28 @@ function assignDefaults(userConfig: { [key: string]: any }) {
351351
)
352352
}
353353
}
354+
355+
if (
356+
typeof images.dangerouslyAllowSVG !== 'undefined' &&
357+
typeof images.dangerouslyAllowSVG !== 'boolean'
358+
) {
359+
throw new Error(
360+
`Specified images.dangerouslyAllowSVG should be a boolean
361+
', '
362+
)}), received (${images.dangerouslyAllowSVG}).\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
363+
)
364+
}
365+
366+
if (
367+
typeof images.contentSecurityPolicy !== 'undefined' &&
368+
typeof images.contentSecurityPolicy !== 'string'
369+
) {
370+
throw new Error(
371+
`Specified images.contentSecurityPolicy should be a string
372+
', '
373+
)}), received (${images.contentSecurityPolicy}).\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
374+
)
375+
}
354376
}
355377

356378
if (result.webpack5 === false) {

packages/next/server/image-config.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,22 @@ export type ImageConfigComplete = {
2929
path: string
3030

3131
/** @see [Image domains configuration](https://nextjs.org/docs/basic-features/image-optimization#domains) */
32-
domains?: string[]
32+
domains: string[]
3333

3434
/** @see [Cache behavior](https://nextjs.org/docs/api-reference/next/image#caching-behavior) */
35-
disableStaticImages?: boolean
35+
disableStaticImages: boolean
3636

3737
/** @see [Cache behavior](https://nextjs.org/docs/api-reference/next/image#caching-behavior) */
38-
minimumCacheTTL?: number
38+
minimumCacheTTL: number
3939

4040
/** @see [Acceptable formats](https://nextjs.org/docs/api-reference/next/image#acceptable-formats) */
41-
formats?: ImageFormat[]
41+
formats: ImageFormat[]
42+
43+
/** @see [Dangerously Allow SVG](https://nextjs.org/docs/api-reference/next/image#dangerously-allow-svg) */
44+
dangerouslyAllowSVG: boolean
45+
46+
/** @see [Dangerously Allow SVG](https://nextjs.org/docs/api-reference/next/image#dangerously-allow-svg) */
47+
contentSecurityPolicy: string
4248
}
4349

4450
export type ImageConfig = Partial<ImageConfigComplete>
@@ -52,4 +58,6 @@ export const imageConfigDefault: ImageConfigComplete = {
5258
disableStaticImages: false,
5359
minimumCacheTTL: 60,
5460
formats: ['image/webp'],
61+
dangerouslyAllowSVG: false,
62+
contentSecurityPolicy: `script-src 'none'; frame-src 'none'; sandbox;`,
5563
}

packages/next/server/image-optimizer.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,16 @@ export async function imageOptimizer(
380380
}
381381
}
382382

383+
if (upstreamType === SVG && !nextConfig.images.dangerouslyAllowSVG) {
384+
console.error(
385+
`The requested resource "${href}" has type "${upstreamType}" but dangerouslyAllowSVG is disabled`
386+
)
387+
throw new ImageError(
388+
400,
389+
'"url" parameter is valid but image type is not allowed'
390+
)
391+
}
392+
383393
if (upstreamType) {
384394
const vector = VECTOR_TYPES.includes(upstreamType)
385395
const animate =
@@ -576,14 +586,15 @@ function getFileNameWithExtension(
576586
return `${fileName}.${extension}`
577587
}
578588

579-
export function setResponseHeaders(
589+
function setResponseHeaders(
580590
req: IncomingMessage,
581591
res: ServerResponse,
582592
url: string,
583593
etag: string,
584594
contentType: string | null,
585595
isStatic: boolean,
586-
xCache: XCacheHeader
596+
xCache: XCacheHeader,
597+
contentSecurityPolicy: string
587598
) {
588599
res.setHeader('Vary', 'Accept')
589600
res.setHeader(
@@ -608,7 +619,9 @@ export function setResponseHeaders(
608619
)
609620
}
610621

611-
res.setHeader('Content-Security-Policy', `script-src 'none'; sandbox;`)
622+
if (contentSecurityPolicy) {
623+
res.setHeader('Content-Security-Policy', contentSecurityPolicy)
624+
}
612625
res.setHeader('X-Nextjs-Cache', xCache)
613626

614627
return { finished: false }
@@ -621,7 +634,8 @@ export function sendResponse(
621634
extension: string,
622635
buffer: Buffer,
623636
isStatic: boolean,
624-
xCache: XCacheHeader
637+
xCache: XCacheHeader,
638+
contentSecurityPolicy: string
625639
) {
626640
const contentType = getContentType(extension)
627641
const etag = getHash([buffer])
@@ -632,7 +646,8 @@ export function sendResponse(
632646
etag,
633647
contentType,
634648
isStatic,
635-
xCache
649+
xCache,
650+
contentSecurityPolicy
636651
)
637652
if (!result.finished) {
638653
res.end(buffer)

packages/next/server/next-server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ export default class NextNodeServer extends BaseServer {
255255
cacheEntry.value.extension,
256256
cacheEntry.value.buffer,
257257
paramsResult.isStatic,
258-
cacheEntry.isMiss ? 'MISS' : cacheEntry.isStale ? 'STALE' : 'HIT'
258+
cacheEntry.isMiss ? 'MISS' : cacheEntry.isStale ? 'STALE' : 'HIT',
259+
imagesConfig.contentSecurityPolicy
259260
)
260261
} catch (err) {
261262
if (err instanceof ImageError) {

test/integration/image-component/default/test/index.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ function runTests(mode) {
208208
)
209209
await check(
210210
() => browser.eval(`document.getElementById("img3").currentSrc`),
211-
/test(.*)svg/
211+
/test\.svg/
212212
)
213213
await check(
214214
() => browser.eval(`document.getElementById("img4").currentSrc`),
@@ -224,7 +224,7 @@ function runTests(mode) {
224224
)
225225
await check(
226226
() => browser.eval(`document.getElementById("msg3").textContent`),
227-
'loaded 1 img3 with dimensions 266x266'
227+
'loaded 1 img3 with dimensions 400x400'
228228
)
229229
await check(
230230
() => browser.eval(`document.getElementById("msg4").textContent`),
@@ -1077,7 +1077,7 @@ function runTests(mode) {
10771077
expect(
10781078
await hasImageMatchingUrl(
10791079
browser,
1080-
`http://localhost:${appPort}/_next/image?url=%2Ftest.svg&w=828&q=75`
1080+
`http://localhost:${appPort}/test.svg`
10811081
)
10821082
).toBe(true)
10831083
expect(

test/integration/image-optimizer/test/index.test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,56 @@ describe('Image Optimizer', () => {
224224
`Specified images.loader property (imgix) also requires images.path property to be assigned to a URL prefix.`
225225
)
226226
})
227+
228+
it('should error when images.dangerouslyAllowSVG is not a boolean', async () => {
229+
await nextConfig.replace(
230+
'{ /* replaceme */ }',
231+
JSON.stringify({
232+
images: {
233+
dangerouslyAllowSVG: 'foo',
234+
},
235+
})
236+
)
237+
let stderr = ''
238+
239+
app = await launchApp(appDir, await findPort(), {
240+
onStderr(msg) {
241+
stderr += msg || ''
242+
},
243+
})
244+
await waitFor(1000)
245+
await killApp(app).catch(() => {})
246+
await nextConfig.restore()
247+
248+
expect(stderr).toContain(
249+
`Specified images.dangerouslyAllowSVG should be a boolean`
250+
)
251+
})
252+
253+
it('should error when images.contentSecurityPolicy is not a string', async () => {
254+
await nextConfig.replace(
255+
'{ /* replaceme */ }',
256+
JSON.stringify({
257+
images: {
258+
contentSecurityPolicy: 1,
259+
},
260+
})
261+
)
262+
let stderr = ''
263+
264+
app = await launchApp(appDir, await findPort(), {
265+
onStderr(msg) {
266+
stderr += msg || ''
267+
},
268+
})
269+
await waitFor(1000)
270+
await killApp(app).catch(() => {})
271+
await nextConfig.restore()
272+
273+
expect(stderr).toContain(
274+
`Specified images.contentSecurityPolicy should be a string`
275+
)
276+
})
227277
})
228278

229279
// domains for testing
@@ -240,11 +290,13 @@ describe('Image Optimizer', () => {
240290

241291
describe('Server support for minimumCacheTTL in next.config.js', () => {
242292
const size = 96 // defaults defined in server/config.ts
293+
const dangerouslyAllowSVG = true
243294
const ctx = {
244295
w: size,
245296
isDev: false,
246297
domains,
247298
minimumCacheTTL,
299+
dangerouslyAllowSVG,
248300
imagesDir,
249301
appDir,
250302
}
@@ -253,6 +305,7 @@ describe('Image Optimizer', () => {
253305
images: {
254306
domains,
255307
minimumCacheTTL,
308+
dangerouslyAllowSVG,
256309
},
257310
})
258311
ctx.nextOutput = ''

0 commit comments

Comments
 (0)