Skip to content

Commit

Permalink
feat: add contentDispositionType config to Image Optimization API (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
styfle authored Feb 22, 2023
1 parent 3d73366 commit 57d2963
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 61 deletions.
6 changes: 5 additions & 1 deletion docs/api-reference/next/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ description: Enable Image Optimization with the built-in Image component.

| Version | Changes |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `v13.2.0` | `contentDispositionType` configuration added. |
| `v13.0.6` | `ref` prop added. |
| `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. |
| `v12.3.0` | `remotePatterns` and `unoptimized` configuration is stable. |
Expand Down Expand Up @@ -503,17 +504,20 @@ module.exports = {

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

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

```js
module.exports = {
images: {
dangerouslyAllowSVG: true,
contentDispositionType: 'attachment',
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
}
```

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.

### Animated Images

The default [loader](#loader) will automatically bypass Image Optimization for animated images and serve the image as-is.
Expand Down
5 changes: 4 additions & 1 deletion docs/api-reference/next/legacy/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -571,17 +571,20 @@ module.exports = {

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

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

```js
module.exports = {
images: {
dangerouslyAllowSVG: true,
contentDispositionType: 'attachment',
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
}
```

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.

### Animated Images

The default [loader](#loader) will automatically bypass Image Optimization for animated images and serve the image as-is.
Expand Down
2 changes: 2 additions & 0 deletions errors/invalid-images-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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',
// limit of 50 objects
remotePatterns: [],
// when true, every image will be unoptimized
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,10 @@ const configSchema = {
minLength: 1,
type: 'string',
},
contentDispositionType: {
enum: ['inline', 'attachment'] as any, // automatic typing does not like enum
type: 'string',
},
dangerouslyAllowSVG: {
type: 'boolean',
},
Expand Down
25 changes: 11 additions & 14 deletions packages/next/src/server/image-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { IncrementalCacheEntry, IncrementalCacheValue } from './response-cache'
import { mockRequest } from './lib/mock-request'
import { hasMatch } from '../shared/lib/match-remote-pattern'
import { getImageBlurSvg } from '../shared/lib/image-blur-svg'
import { ImageConfigComplete } from '../shared/lib/image-config'

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

Expand Down Expand Up @@ -672,11 +673,11 @@ export async function imageOptimizer(
function getFileNameWithExtension(
url: string,
contentType: string | null
): string | void {
): string {
const [urlWithoutQueryParams] = url.split('?')
const fileNameWithExtension = urlWithoutQueryParams.split('/').pop()
if (!contentType || !fileNameWithExtension) {
return
return 'image.bin'
}

const [fileName] = fileNameWithExtension.split('.')
Expand All @@ -692,7 +693,7 @@ function setResponseHeaders(
contentType: string | null,
isStatic: boolean,
xCache: XCacheHeader,
contentSecurityPolicy: string,
imagesConfig: ImageConfigComplete,
maxAge: number,
isDev: boolean
) {
Expand All @@ -712,16 +713,12 @@ function setResponseHeaders(
}

const fileName = getFileNameWithExtension(url, contentType)
if (fileName) {
res.setHeader(
'Content-Disposition',
contentDisposition(fileName, { type: 'inline' })
)
}
res.setHeader(
'Content-Disposition',
contentDisposition(fileName, { type: imagesConfig.contentDispositionType })
)

if (contentSecurityPolicy) {
res.setHeader('Content-Security-Policy', contentSecurityPolicy)
}
res.setHeader('Content-Security-Policy', imagesConfig.contentSecurityPolicy)
res.setHeader('X-Nextjs-Cache', xCache)

return { finished: false }
Expand All @@ -735,7 +732,7 @@ export function sendResponse(
buffer: Buffer,
isStatic: boolean,
xCache: XCacheHeader,
contentSecurityPolicy: string,
imagesConfig: ImageConfigComplete,
maxAge: number,
isDev: boolean
) {
Expand All @@ -749,7 +746,7 @@ export function sendResponse(
contentType,
isStatic,
xCache,
contentSecurityPolicy,
imagesConfig,
maxAge,
isDev
)
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ export default class NextNodeServer extends BaseServer {
cacheEntry.value.buffer,
paramsResult.isStatic,
cacheEntry.isMiss ? 'MISS' : cacheEntry.isStale ? 'STALE' : 'HIT',
imagesConfig.contentSecurityPolicy,
imagesConfig,
cacheEntry.revalidate || 0,
Boolean(this.renderOpts.dev)
)
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/shared/lib/image-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ export type ImageConfigComplete = {
/** @see [Dangerously Allow SVG](https://nextjs.org/docs/api-reference/next/image#dangerously-allow-svg) */
contentSecurityPolicy: string

/** @see [Dangerously Allow SVG](https://nextjs.org/docs/api-reference/next/image#dangerously-allow-svg) */
contentDispositionType: 'inline' | 'attachment'

/** @see [Remote Patterns](https://nextjs.org/docs/api-reference/next/image#remote-patterns) */
remotePatterns: RemotePattern[]

Expand All @@ -109,6 +112,7 @@ export const imageConfigDefault: ImageConfigComplete = {
formats: ['image/webp'],
dangerouslyAllowSVG: false,
contentSecurityPolicy: `script-src 'none'; frame-src 'none'; sandbox;`,
contentDispositionType: 'inline',
remotePatterns: [],
unoptimized: false,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { join } from 'path'
import { setupTests } from './util'

const appDir = join(__dirname, '../app')
const imagesDir = join(appDir, '.next', 'cache', 'images')

describe('with contentDispositionType attachment', () => {
setupTests({
nextConfigImages: { contentDispositionType: 'attachment' },
appDir,
imagesDir,
})
})
Loading

0 comments on commit 57d2963

Please sign in to comment.