Skip to content

Commit 57d2963

Browse files
authored
feat: add contentDispositionType config to Image Optimization API (#46254)
Add `contentDispositionType` config to Image Optimization API so the user can configure `inline` vs `attachment`. This is recommended when `dangerouslyAllowSVG` is enabled but can also be used when its disabled.
1 parent 3d73366 commit 57d2963

File tree

9 files changed

+99
-61
lines changed

9 files changed

+99
-61
lines changed

docs/api-reference/next/image.md

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

1717
| Version | Changes |
1818
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
19+
| `v13.2.0` | `contentDispositionType` configuration added. |
1920
| `v13.0.6` | `ref` prop added. |
2021
| `v13.0.0` | `<span>` wrapper removed. `layout`, `objectFit`, `objectPosition`, `lazyBoundary`, `lazyRoot` props removed. `alt` is required. `onLoadingComplete` receives reference to `img` element. Built-in loader config removed. |
2122
| `v12.3.0` | `remotePatterns` and `unoptimized` configuration is stable. |
@@ -503,17 +504,20 @@ module.exports = {
503504

504505
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).
505506

506-
If you need to serve SVG images with the default Image Optimization API, you can set `dangerouslyAllowSVG` and `contentSecurityPolicy` inside your `next.config.js`:
507+
If you need to serve SVG images with the default Image Optimization API, you can set `dangerouslyAllowSVG` inside your `next.config.js`:
507508

508509
```js
509510
module.exports = {
510511
images: {
511512
dangerouslyAllowSVG: true,
513+
contentDispositionType: 'attachment',
512514
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
513515
},
514516
}
515517
```
516518

519+
In addition, it is strongly recommended to also set `contentDispositionType` to force the browser to download the image, as well as `contentSecurityPolicy` to prevent scripts embedded in the image from executing.
520+
517521
### Animated Images
518522

519523
The default [loader](#loader) will automatically bypass Image Optimization for animated images and serve the image as-is.

docs/api-reference/next/legacy/image.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,17 +571,20 @@ module.exports = {
571571

572572
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).
573573

574-
If you need to serve SVG images with the default Image Optimization API, you can set `dangerouslyAllowSVG` and `contentSecurityPolicy` inside your `next.config.js`:
574+
If you need to serve SVG images with the default Image Optimization API, you can set `dangerouslyAllowSVG` inside your `next.config.js`:
575575

576576
```js
577577
module.exports = {
578578
images: {
579579
dangerouslyAllowSVG: true,
580+
contentDispositionType: 'attachment',
580581
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
581582
},
582583
}
583584
```
584585

586+
In addition, it is strongly recommended to also set `contentDispositionType` to force the browser to download the image, as well as `contentSecurityPolicy` to prevent scripts embedded in the image from executing.
587+
585588
### Animated Images
586589

587590
The default [loader](#loader) will automatically bypass Image Optimization for animated images and serve the image as-is.

errors/invalid-images-config.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ module.exports = {
3333
dangerouslyAllowSVG: false,
3434
// set the Content-Security-Policy header
3535
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
36+
// sets the Content-Disposition header (inline or attachment)
37+
contentDispositionType: 'inline',
3638
// limit of 50 objects
3739
remotePatterns: [],
3840
// when true, every image will be unoptimized

packages/next/src/server/config-schema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,10 @@ const configSchema = {
598598
minLength: 1,
599599
type: 'string',
600600
},
601+
contentDispositionType: {
602+
enum: ['inline', 'attachment'] as any, // automatic typing does not like enum
603+
type: 'string',
604+
},
601605
dangerouslyAllowSVG: {
602606
type: 'boolean',
603607
},

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

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { IncrementalCacheEntry, IncrementalCacheValue } from './response-cache'
2323
import { mockRequest } from './lib/mock-request'
2424
import { hasMatch } from '../shared/lib/match-remote-pattern'
2525
import { getImageBlurSvg } from '../shared/lib/image-blur-svg'
26+
import { ImageConfigComplete } from '../shared/lib/image-config'
2627

2728
type XCacheHeader = 'MISS' | 'HIT' | 'STALE'
2829

@@ -672,11 +673,11 @@ export async function imageOptimizer(
672673
function getFileNameWithExtension(
673674
url: string,
674675
contentType: string | null
675-
): string | void {
676+
): string {
676677
const [urlWithoutQueryParams] = url.split('?')
677678
const fileNameWithExtension = urlWithoutQueryParams.split('/').pop()
678679
if (!contentType || !fileNameWithExtension) {
679-
return
680+
return 'image.bin'
680681
}
681682

682683
const [fileName] = fileNameWithExtension.split('.')
@@ -692,7 +693,7 @@ function setResponseHeaders(
692693
contentType: string | null,
693694
isStatic: boolean,
694695
xCache: XCacheHeader,
695-
contentSecurityPolicy: string,
696+
imagesConfig: ImageConfigComplete,
696697
maxAge: number,
697698
isDev: boolean
698699
) {
@@ -712,16 +713,12 @@ function setResponseHeaders(
712713
}
713714

714715
const fileName = getFileNameWithExtension(url, contentType)
715-
if (fileName) {
716-
res.setHeader(
717-
'Content-Disposition',
718-
contentDisposition(fileName, { type: 'inline' })
719-
)
720-
}
716+
res.setHeader(
717+
'Content-Disposition',
718+
contentDisposition(fileName, { type: imagesConfig.contentDispositionType })
719+
)
721720

722-
if (contentSecurityPolicy) {
723-
res.setHeader('Content-Security-Policy', contentSecurityPolicy)
724-
}
721+
res.setHeader('Content-Security-Policy', imagesConfig.contentSecurityPolicy)
725722
res.setHeader('X-Nextjs-Cache', xCache)
726723

727724
return { finished: false }
@@ -735,7 +732,7 @@ export function sendResponse(
735732
buffer: Buffer,
736733
isStatic: boolean,
737734
xCache: XCacheHeader,
738-
contentSecurityPolicy: string,
735+
imagesConfig: ImageConfigComplete,
739736
maxAge: number,
740737
isDev: boolean
741738
) {
@@ -749,7 +746,7 @@ export function sendResponse(
749746
contentType,
750747
isStatic,
751748
xCache,
752-
contentSecurityPolicy,
749+
imagesConfig,
753750
maxAge,
754751
isDev
755752
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ export default class NextNodeServer extends BaseServer {
466466
cacheEntry.value.buffer,
467467
paramsResult.isStatic,
468468
cacheEntry.isMiss ? 'MISS' : cacheEntry.isStale ? 'STALE' : 'HIT',
469-
imagesConfig.contentSecurityPolicy,
469+
imagesConfig,
470470
cacheEntry.revalidate || 0,
471471
Boolean(this.renderOpts.dev)
472472
)

packages/next/src/shared/lib/image-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ export type ImageConfigComplete = {
8888
/** @see [Dangerously Allow SVG](https://nextjs.org/docs/api-reference/next/image#dangerously-allow-svg) */
8989
contentSecurityPolicy: string
9090

91+
/** @see [Dangerously Allow SVG](https://nextjs.org/docs/api-reference/next/image#dangerously-allow-svg) */
92+
contentDispositionType: 'inline' | 'attachment'
93+
9194
/** @see [Remote Patterns](https://nextjs.org/docs/api-reference/next/image#remote-patterns) */
9295
remotePatterns: RemotePattern[]
9396

@@ -109,6 +112,7 @@ export const imageConfigDefault: ImageConfigComplete = {
109112
formats: ['image/webp'],
110113
dangerouslyAllowSVG: false,
111114
contentSecurityPolicy: `script-src 'none'; frame-src 'none'; sandbox;`,
115+
contentDispositionType: 'inline',
112116
remotePatterns: [],
113117
unoptimized: false,
114118
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { join } from 'path'
2+
import { setupTests } from './util'
3+
4+
const appDir = join(__dirname, '../app')
5+
const imagesDir = join(appDir, '.next', 'cache', 'images')
6+
7+
describe('with contentDispositionType attachment', () => {
8+
setupTests({
9+
nextConfigImages: { contentDispositionType: 'attachment' },
10+
appDir,
11+
imagesDir,
12+
})
13+
})

0 commit comments

Comments
 (0)