Skip to content

Commit da8d198

Browse files
authored
Add experimental wildcard remotePatterns config for upstream images (#36245)
## Description This PR implements a new configuration object in `next.config.js` called `experimental.images.remotePatterns`. This will eventually deprecate `images.domains` because it covers the same use cases and more by allowing wildcard pattern matching on `hostname` and `pathname` and also allows restricting `protocol` and `port`. ## Feature - [x] Implements an existing feature request. - [x] Related issues linked - [x] Unit tests added - [x] Integration tests added - [x] Documentation added - [x] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md` ## Related - Fixes #27925 - Closes #18429 - Closes #18632 - Closes #18730 - Closes #27345
1 parent 0dd6211 commit da8d198

File tree

19 files changed

+583
-34
lines changed

19 files changed

+583
-34
lines changed

docs/api-reference/next/image.md

+57-1
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+
| `v12.1.7` | Experimental `remotePatterns` configuration added. |
1920
| `v12.1.1` | `style` prop added. Experimental[\*](#experimental-raw-layout-mode) support for `layout="raw"` added. |
2021
| `v12.1.0` | `dangerouslyAllowSVG` and `contentSecurityPolicy` configuration added. |
2122
| `v12.0.9` | `lazyRoot` prop added. |
@@ -313,9 +314,64 @@ Other properties on the `<Image />` component will be passed to the underlying
313314

314315
## Configuration Options
315316

317+
### Remote Patterns
318+
319+
> Note: The `remotePatterns` configuration is currently **experimental** and subject to change. Please use [`domains`](#domains) for production use cases.
320+
321+
To protect your application from malicious users, configuration is required in order to use external images. This ensures that only external images from your account can be served from the Next.js Image Optimization API. These external images can be configured with the `remotePatterns` property in your `next.config.js` file, as shown below:
322+
323+
```js
324+
module.exports = {
325+
experimental: {
326+
images: {
327+
remotePatterns: [
328+
{
329+
protocol: 'https',
330+
hostname: 'example.com',
331+
port: '',
332+
pathname: '/account123/**',
333+
},
334+
],
335+
},
336+
},
337+
}
338+
```
339+
340+
> Note: The example above will ensure the `src` property of `next/image` must start with `https://example.com/account123/`. Any other protocol, hostname, port, or unmatched path will respond with 400 Bad Request.
341+
342+
Below is another example of the `remotePatterns` property in the `next.config.js` file:
343+
344+
```js
345+
module.exports = {
346+
experimental: {
347+
images: {
348+
remotePatterns: [
349+
{
350+
protocol: 'https',
351+
hostname: '**.example.com',
352+
},
353+
],
354+
},
355+
},
356+
}
357+
```
358+
359+
> Note: The example above will ensure the `src` property of `next/image` must start with `https://img1.example.com` or `https://me.avatar.example.com` or any number of subdomains. Any other protocol or unmatched hostname will respond with 400 Bad Request.
360+
361+
Wildcard patterns can be used for both `pathname` and `hostname` and have the following syntax:
362+
363+
- `*` match a single path segment or subdomain
364+
- `**` match any number of path segments at the end or subdomains at the beginning
365+
366+
The `**` syntax does not work in the middle of the pattern.
367+
316368
### Domains
317369

318-
To protect your application from malicious users, you must define a list of image provider domains that you want to be served from the Next.js Image Optimization API. This is configured in with the `domains` property in your `next.config.js` file, as shown below:
370+
Similar to [`remotePatterns`](#remote-patterns), the `domains` configuration can be used to provide a list of allowed hostnames for external images.
371+
372+
However, the `domains` configuration does not support wildcard pattern matching and it cannot restrict protocol, port, or pathname.
373+
374+
Below is an example of the `domains` property in the `next.config.js` file:
319375

320376
```js
321377
module.exports = {

docs/basic-features/image-optimization.md

+4-10
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ function Home() {
6666

6767
### Remote Images
6868

69-
To use a remote image, the `src` property should be a URL string, which can be [relative](#loaders) or [absolute](#domains). Because Next.js does not have access to remote files during the build process, you'll need to provide the [`width`](/docs/api-reference/next/image.md#width), [`height`](/docs/api-reference/next/image.md#height) and optional [`blurDataURL`](/docs/api-reference/next/image.md#blurdataurl) props manually:
69+
To use a remote image, the `src` property should be a URL string, which can be [relative](#loaders) or [absolute](/docs/api-reference/next/image.md#domains). Because Next.js does not have access to remote files during the build process, you'll need to provide the [`width`](/docs/api-reference/next/image.md#width), [`height`](/docs/api-reference/next/image.md#height) and optional [`blurDataURL`](/docs/api-reference/next/image.md#blurdataurl) props manually:
7070

7171
```jsx
7272
import Image from 'next/image'
@@ -93,15 +93,9 @@ export default function Home() {
9393

9494
Sometimes you may want to access a remote image, but still use the built-in Next.js Image Optimization API. To do this, leave the `loader` at its default setting and enter an absolute URL for the Image `src`.
9595

96-
To protect your application from malicious users, you must define a list of remote domains that you intend to access this way. This is configured in your `next.config.js` file, as shown below:
96+
To protect your application from malicious users, you must define a list of remote hostnames you intend to allow remote access.
9797

98-
```js
99-
module.exports = {
100-
images: {
101-
domains: ['example.com', 'example2.com'],
102-
},
103-
}
104-
```
98+
> Learn more about [`domains`](/docs/api-reference/next/image.md#domains) configuration.
10599
106100
### Loaders
107101

@@ -207,7 +201,7 @@ For examples of the Image component used with the various fill modes, see the [I
207201

208202
## Configuration
209203

210-
The `next/image` component and Next.js Image Optimization API can be configured in the [`next.config.js` file](/docs/api-reference/next.config.js/introduction.md). These configurations allow you to [enable remote domains](/docs/api-reference/next/image.md#domains), [define custom image breakpoints](/docs/api-reference/next/image.md#device-sizes), [change caching behavior](/docs/api-reference/next/image.md#caching-behavior) and more.
204+
The `next/image` component and Next.js Image Optimization API can be configured in the [`next.config.js` file](/docs/api-reference/next.config.js/introduction.md). These configurations allow you to [enable remote images](/docs/api-reference/next/image.md#domains), [define custom image breakpoints](/docs/api-reference/next/image.md#device-sizes), [change caching behavior](/docs/api-reference/next/image.md#caching-behavior) and more.
211205

212206
[**Read the full image configuration documentation for more information.**](/docs/api-reference/next/image.md#configuration-options)
213207

errors/invalid-images-config.md

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ module.exports = {
1717
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
1818
// limit of 50 domains values
1919
domains: [],
20+
// limit of 50 objects
21+
remotePatterns: [],
2022
// path prefix for Image Optimization API, useful with `loader`
2123
path: '/_next/image',
2224
// loader can be 'default', 'imgix', 'cloudinary', 'akamai', or 'custom'

packages/next/build/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2132,6 +2132,8 @@ export default async function build(
21322132
const images = { ...config.images }
21332133
const { deviceSizes, imageSizes } = images
21342134
;(images as any).sizes = [...deviceSizes, ...imageSizes]
2135+
;(images as any).remotePatterns =
2136+
config?.experimental?.images?.remotePatterns || []
21352137

21362138
await promises.writeFile(
21372139
path.join(distDir, IMAGES_MANIFEST),

packages/next/build/webpack-config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,8 @@ export default async function getBaseWebpackConfig(
14671467
? {
14681468
// pass domains in development to allow validating on the client
14691469
domains: config.images.domains,
1470+
experimentalRemotePatterns:
1471+
config.experimental?.images?.remotePatterns,
14701472
}
14711473
: {}),
14721474
}),

packages/next/client/image.tsx

+15-11
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import { ImageConfigContext } from '../shared/lib/image-config-context'
1818
import { warnOnce } from '../shared/lib/utils'
1919
import { normalizePathTrailingSlash } from './normalize-trailing-slash'
2020

21-
const experimentalLayoutRaw = (process.env.__NEXT_IMAGE_OPTS as any)
22-
?.experimentalLayoutRaw
21+
const { experimentalLayoutRaw = false, experimentalRemotePatterns = [] } =
22+
(process.env.__NEXT_IMAGE_OPTS as any) || {}
2323
const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
2424
const loadedImageURLs = new Set<string>()
2525
const allImgs = new Map<
@@ -1063,7 +1063,10 @@ function defaultLoader({
10631063
)
10641064
}
10651065

1066-
if (!src.startsWith('/') && config.domains) {
1066+
if (
1067+
!src.startsWith('/') &&
1068+
(config.domains || experimentalRemotePatterns)
1069+
) {
10671070
let parsedSrc: URL
10681071
try {
10691072
parsedSrc = new URL(src)
@@ -1074,14 +1077,15 @@ function defaultLoader({
10741077
)
10751078
}
10761079

1077-
if (
1078-
process.env.NODE_ENV !== 'test' &&
1079-
!config.domains.includes(parsedSrc.hostname)
1080-
) {
1081-
throw new Error(
1082-
`Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` +
1083-
`See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host`
1084-
)
1080+
if (process.env.NODE_ENV !== 'test') {
1081+
// We use dynamic require because this should only error in development
1082+
const { hasMatch } = require('../shared/lib/match-remote-pattern')
1083+
if (!hasMatch(config.domains, experimentalRemotePatterns, parsedSrc)) {
1084+
throw new Error(
1085+
`Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` +
1086+
`See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host`
1087+
)
1088+
}
10851089
}
10861090
}
10871091
}

packages/next/server/config-shared.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ImageConfig,
66
ImageConfigComplete,
77
imageConfigDefault,
8+
RemotePattern,
89
} from '../shared/lib/image-config'
910

1011
export type PageRuntime = 'nodejs' | 'edge' | undefined
@@ -115,6 +116,7 @@ export interface ExperimentalConfig {
115116
outputStandalone?: boolean
116117
images?: {
117118
layoutRaw: boolean
119+
remotePatterns: RemotePattern[]
118120
}
119121
middlewareSourceMaps?: boolean
120122
emotion?:
@@ -501,6 +503,7 @@ export const defaultConfig: NextConfig = {
501503
outputStandalone: !!process.env.NEXT_PRIVATE_STANDALONE,
502504
images: {
503505
layoutRaw: false,
506+
remotePatterns: [],
504507
},
505508
},
506509
}

packages/next/server/config.ts

+37
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,43 @@ function assignDefaults(userConfig: { [key: string]: any }) {
234234
)
235235
}
236236
}
237+
238+
const remotePatterns = result.experimental?.images?.remotePatterns
239+
if (remotePatterns) {
240+
if (!Array.isArray(remotePatterns)) {
241+
throw new Error(
242+
`Specified images.remotePatterns should be an Array received ${typeof remotePatterns}.\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
243+
)
244+
}
245+
246+
if (remotePatterns.length > 50) {
247+
throw new Error(
248+
`Specified images.remotePatterns exceeds length of 50, received length (${remotePatterns.length}), please reduce the length of the array to continue.\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
249+
)
250+
}
251+
252+
const validProps = new Set(['protocol', 'hostname', 'pathname', 'port'])
253+
const requiredProps = ['hostname']
254+
const invalidPatterns = remotePatterns.filter(
255+
(d: unknown) =>
256+
!d ||
257+
typeof d !== 'object' ||
258+
Object.entries(d).some(
259+
([k, v]) => !validProps.has(k) || typeof v !== 'string'
260+
) ||
261+
requiredProps.some((k) => !(k in d))
262+
)
263+
if (invalidPatterns.length > 0) {
264+
throw new Error(
265+
`Invalid images.remotePatterns values:\n${invalidPatterns
266+
.map((item) => JSON.stringify(item))
267+
.join(
268+
'\n'
269+
)}\n\nremotePatterns value must follow format { protocol: 'https', hostname: 'example.com', port: '', pathname: '/imgs/**' }.\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
270+
)
271+
}
272+
}
273+
237274
if (images.deviceSizes) {
238275
const { deviceSizes } = images
239276
if (!Array.isArray(deviceSizes)) {

packages/next/server/image-optimizer.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import chalk from 'next/dist/compiled/chalk'
1717
import { NextUrlWithParsedQuery } from './request-meta'
1818
import { IncrementalCacheEntry, IncrementalCacheValue } from './response-cache'
1919
import { mockRequest } from './lib/mock-request'
20+
import { hasMatch } from '../shared/lib/match-remote-pattern'
2021

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

@@ -75,6 +76,7 @@ export class ImageOptimizerCache {
7576
minimumCacheTTL = 60,
7677
formats = ['image/webp'],
7778
} = imageData
79+
const remotePatterns = nextConfig.experimental.images?.remotePatterns || []
7880
const { url, w, q } = query
7981
let href: string
8082

@@ -104,7 +106,7 @@ export class ImageOptimizerCache {
104106
return { errorMessage: '"url" parameter is invalid' }
105107
}
106108

107-
if (!domains || !domains.includes(hrefParsed.hostname)) {
109+
if (!hasMatch(domains, remotePatterns, hrefParsed)) {
108110
return { errorMessage: '"url" parameter is not allowed' }
109111
}
110112
}

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

+30-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,33 @@ export const VALID_LOADERS = [
88

99
export type LoaderValue = typeof VALID_LOADERS[number]
1010

11+
export type RemotePattern = {
12+
/**
13+
* Must be `http` or `https`.
14+
*/
15+
protocol?: 'http' | 'https'
16+
17+
/**
18+
* Can be literal or wildcard.
19+
* Single `*` matches a single subdomain.
20+
* Double `**` matches any number of subdomains.
21+
*/
22+
hostname: string
23+
24+
/**
25+
* Can be literal port such as `8080` or empty string
26+
* meaning no port.
27+
*/
28+
port?: string
29+
30+
/**
31+
* Can be literal or wildcard.
32+
* Single `*` matches a single path segment.
33+
* Double `**` matches any number of path segments.
34+
*/
35+
pathname?: string
36+
}
37+
1138
type ImageFormat = 'image/avif' | 'image/webp'
1239

1340
/**
@@ -28,7 +55,9 @@ export type ImageConfigComplete = {
2855
/** @see [Image loader configuration](https://nextjs.org/docs/api-reference/next/image#loader-configuration) */
2956
path: string
3057

31-
/** @see [Image domains configuration](https://nextjs.org/docs/basic-features/image-optimization#domains) */
58+
/**
59+
* @see [Image domains configuration](https://nextjs.org/docs/api-reference/next/image#domains)
60+
*/
3261
domains: string[]
3362

3463
/** @see [Cache behavior](https://nextjs.org/docs/api-reference/next/image#caching-behavior) */

0 commit comments

Comments
 (0)