Skip to content

Commit

Permalink
feat!(next/image): change default Content-Disposition to `attachmen…
Browse files Browse the repository at this point in the history
…t` (#65631)

### BREAKING CHANGE

This changes the behavior of the default image `loader` so that
[`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_response_header_for_the_main_body)
header is now `attachment` for added protection since the API can serve
arbitrary remote images.

The new default value, `attachment`, forces the browser to download the
image when visiting directly. This is particularly important when
`dangerouslyAllowSVG` is true. Most users will not notice the change
since visiting pages won't behave any differently, only visiting images
directly.

Users can switch back to the old behavior by configuring `inline` in
next.config.js

```js
module.exports = {
  images: {
    contentDispositionType: 'inline',
  },
}
  • Loading branch information
styfle committed May 11, 2024
1 parent b9af6a9 commit 292fd4e
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 127 deletions.
17 changes: 17 additions & 0 deletions docs/02-app/02-api-reference/01-components/image.mdx
Expand Up @@ -725,6 +725,22 @@ module.exports = {

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.

### `contentDispositionType`

The default [loader](#loader) sets the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_response_header_for_the_main_body) header to `attachment` for added protection since the API can serve arbitrary remote images.

The default value is `attachment` which forces the browser to download the image when visiting directly. This is particularly important when [`dangerouslyAllowSVG`](#dangerouslyallowsvg) is true.

You can optionally configure `inline` to allow the browser to render the image when visiting directly, without downloading it.

```js filename="next.config.js"
module.exports = {
images: {
contentDispositionType: 'inline',
},
}
```

## Animated Images

The default [loader](#loader) will automatically bypass Image Optimization for animated images and serve the image as-is.
Expand Down Expand Up @@ -1000,6 +1016,7 @@ This `next/image` component uses browser native [lazy loading](https://caniuse.c

| Version | Changes |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `v15.0.0` | `contentDispositionType` configuration default changed to `attachment`. |
| `v14.2.0` | `overrideSrc` prop added. |
| `v14.1.0` | `getImageProps()` is stable. |
| `v14.0.0` | `onLoadingComplete` prop and `domains` config deprecated. |
Expand Down
16 changes: 16 additions & 0 deletions docs/03-pages/02-api-reference/01-components/image-legacy.mdx
Expand Up @@ -577,6 +577,22 @@ module.exports = {

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.

### `contentDispositionType`

The default [loader](#loader) sets the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_response_header_for_the_main_body) header to `attachment` for added protection since the API can serve arbitrary remote images.

The default value is `attachment` which forces the browser to download the image when visiting directly. This is particularly important when [`dangerouslyAllowSVG`](#dangerously-allow-svg) is true.

You can optionally configure `inline` to allow the browser to render the image when visiting directly, without downloading it.

```js filename="next.config.js"
module.exports = {
images: {
contentDispositionType: 'inline',
},
}
```

### Animated Images

The default [loader](#loader) will automatically bypass Image Optimization for animated images and serve the image as-is.
Expand Down
4 changes: 2 additions & 2 deletions errors/invalid-images-config.mdx
Expand Up @@ -35,8 +35,8 @@ module.exports = {
dangerouslyAllowSVG: false,
// set the Content-Security-Policy header
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
// sets the Content-Disposition header (inline or attachment)
contentDispositionType: 'inline',
// sets the Content-Disposition header ('inline' or 'attachment')
contentDispositionType: 'attachment',
// limit of 50 objects
remotePatterns: [],
// when true, every image will be unoptimized
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/shared/lib/image-config.ts
Expand Up @@ -112,7 +112,7 @@ export const imageConfigDefault: ImageConfigComplete = {
formats: ['image/webp'],
dangerouslyAllowSVG: false,
contentSecurityPolicy: `script-src 'none'; frame-src 'none'; sandbox;`,
contentDispositionType: 'inline',
contentDispositionType: 'attachment',
remotePatterns: [],
unoptimized: false,
}
Expand Up @@ -4,9 +4,9 @@ import { setupTests } from './util'
const appDir = join(__dirname, '../app')
const imagesDir = join(appDir, '.next', 'cache', 'images')

describe('with contentDispositionType attachment', () => {
describe('with contentDispositionType inline', () => {
setupTests({
nextConfigImages: { contentDispositionType: 'attachment' },
nextConfigImages: { contentDispositionType: 'inline' },
appDir,
imagesDir,
})
Expand Down
9 changes: 5 additions & 4 deletions test/integration/image-optimizer/test/index.test.ts
Expand Up @@ -402,8 +402,9 @@ describe('Image Optimizer', () => {

await retry(() => {
expect(stderr).toContain(
`Invalid assetPrefix provided. Original error: TypeError [ERR_INVALID_URL]: Invalid URL`
`Invalid assetPrefix provided. Original error:`
)
expect(stderr).toContain(`Invalid URL`)
})
} finally {
await killApp(app).catch(() => {})
Expand Down Expand Up @@ -584,7 +585,7 @@ describe('Image Optimizer', () => {
`public, max-age=86400, must-revalidate`
)
expect(res.headers.get('Content-Disposition')).toBe(
`inline; filename="test.webp"`
`attachment; filename="test.webp"`
)

await check(async () => {
Expand Down Expand Up @@ -615,7 +616,7 @@ describe('Image Optimizer', () => {
`public, max-age=60, must-revalidate`
)
expect(res.headers.get('Content-Disposition')).toBe(
`inline; filename="test.webp"`
`attachment; filename="test.webp"`
)
})
}
Expand Down Expand Up @@ -723,7 +724,7 @@ describe('Image Optimizer', () => {
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('Content-Disposition')).toBe(
`inline; filename="next-js-bg.webp"`
`attachment; filename="next-js-bg.webp"`
)

await check(async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/integration/image-optimizer/test/util.ts
Expand Up @@ -152,7 +152,7 @@ async function fetchWithDuration(
export function runTests(ctx: RunTestsCtx) {
const { isDev, nextConfigImages } = ctx
const {
contentDispositionType = 'inline',
contentDispositionType = 'attachment',
domains = [],
formats = [],
minimumCacheTTL = 60,
Expand Down
Expand Up @@ -6,6 +6,9 @@ const Page = () => {
<div>
<h1>SVG with a script tag attempting XSS</h1>
<Image id="img" src="/xss.svg" width="100" height="100" />
<a id="btn" href="/_next/image?url=%2Fxss.svg&w=256&q=75">
Click Me
</a>
<p id="msg">safe</p>
</div>
)
Expand Down
6 changes: 2 additions & 4 deletions test/production/pages-dir/production/test/security.ts
Expand Up @@ -326,10 +326,8 @@ export default (next: NextInstance) => {
const src = await browser.elementById('img').getAttribute('src')
expect(src).toMatch(/_next\/image\?.*xss\.svg/)
expect(await browser.elementById('msg').text()).toBe('safe')
browser = await webdriver(
next.appPort,
'/_next/image?url=%2Fxss.svg&w=256&q=75'
)
await browser.eval(`document.getElementById("btn").click()`)
await browser.waitForIdleNetwork()
expect(await browser.elementById('msg').text()).toBe('safe')
} finally {
if (browser) await browser.close()
Expand Down

0 comments on commit 292fd4e

Please sign in to comment.