diff --git a/docs/01-app/03-api-reference/02-components/image.mdx b/docs/01-app/03-api-reference/02-components/image.mdx
index 581d1bb58c5635..9ea97f07889fc7 100644
--- a/docs/01-app/03-api-reference/02-components/image.mdx
+++ b/docs/01-app/03-api-reference/02-components/image.mdx
@@ -528,6 +528,8 @@ module.exports = {
The example above will ensure the `src` property of `next/image` must start with `/assets/images/` and must not have a query string. Attempting to optimize any other path will respond with `400` Bad Request error.
+> **Good to know**: Omitting the `search` property allows all search parameters which could allow malicious actors to optimize URLs you did not intend. Try using a specific value like `search: '?v=2'` to ensure an exact match.
+
#### `remotePatterns`
Use `remotePatterns` in your `next.config.js` file to allow images from specific external paths and block all others. This ensures that only external images from your account can be served.
diff --git a/packages/next/errors.json b/packages/next/errors.json
index 4bd7931befb7e5..752f21dd6cf264 100644
--- a/packages/next/errors.json
+++ b/packages/next/errors.json
@@ -861,5 +861,6 @@
"860": "Client Max Body Size must be a valid number (bytes) or filesize format string (e.g., \"5mb\")",
"861": "Client Max Body Size must be larger than 0 bytes",
"862": "Request body exceeded %s",
- "863": "\\`\\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \\`\\` tag."
+ "863": "\\`\\` received a direct child that is either a Server Component, or JSX that was loaded with React.lazy(). This is not supported. Either remove legacyBehavior, or make the direct child a Client Component that renders the Link's \\`\\` tag.",
+ "864": "Image with src \"%s\" is using a query string which is not configured in images.localPatterns.\\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns"
}
diff --git a/packages/next/src/client/legacy/image.tsx b/packages/next/src/client/legacy/image.tsx
index 3e2a34d47062c4..ab1f589ee69f16 100644
--- a/packages/next/src/client/legacy/image.tsx
+++ b/packages/next/src/client/legacy/image.tsx
@@ -190,14 +190,27 @@ function defaultLoader({
}
}
- const q = findClosestQuality(quality, config)
-
if (!config.dangerouslyAllowSVG && src.split('?', 1)[0].endsWith('.svg')) {
// Special case to make svg serve as-is to avoid proxying
// through the built-in Image Optimization API.
return src
}
+ if (
+ src.startsWith('/') &&
+ src.includes('?') &&
+ (!config.localPatterns?.length ||
+ (config.localPatterns.length === 1 &&
+ config.localPatterns[0].pathname === '/_next/static/media/**'))
+ ) {
+ throw new Error(
+ `Image with src "${src}" is using a query string which is not configured in images.localPatterns.` +
+ `\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns`
+ )
+ }
+
+ const q = findClosestQuality(quality, config)
+
return `${normalizePathTrailingSlash(config.path)}?url=${encodeURIComponent(
src
)}&w=${width}&q=${q}`
@@ -865,19 +878,6 @@ export default function Image({
)
}
- if (
- src.startsWith('/') &&
- src.includes('?') &&
- (!config?.localPatterns?.length ||
- (config.localPatterns.length === 1 &&
- config.localPatterns[0].pathname === '/_next/static/media/**'))
- ) {
- warnOnce(
- `Image with src "${src}" is using a query string which is not configured in images.localPatterns. This config will be required starting in Next.js 16.` +
- `\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns`
- )
- }
-
if (!unoptimized && loader !== defaultImageLoader) {
const urlStr = loader({
config,
diff --git a/packages/next/src/shared/lib/get-img-props.ts b/packages/next/src/shared/lib/get-img-props.ts
index 5fd6cf5825bedb..50073562aa20b2 100644
--- a/packages/next/src/shared/lib/get-img-props.ts
+++ b/packages/next/src/shared/lib/get-img-props.ts
@@ -560,18 +560,6 @@ export function getImgProps(
`\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-qualities`
)
}
- if (
- src.startsWith('/') &&
- src.includes('?') &&
- (!config?.localPatterns?.length ||
- (config.localPatterns.length === 1 &&
- config.localPatterns[0].pathname === '/_next/static/media/**'))
- ) {
- warnOnce(
- `Image with src "${src}" is using a query string which is not configured in images.localPatterns. This config will be required starting in Next.js 16.` +
- `\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns`
- )
- }
if (placeholder === 'blur' && !blurDataURL) {
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next-image-loader
diff --git a/packages/next/src/shared/lib/image-loader.ts b/packages/next/src/shared/lib/image-loader.ts
index 5eaa61217b8ab8..ea3c902327561c 100644
--- a/packages/next/src/shared/lib/image-loader.ts
+++ b/packages/next/src/shared/lib/image-loader.ts
@@ -79,6 +79,19 @@ function defaultLoader({
}
}
+ if (
+ src.startsWith('/') &&
+ src.includes('?') &&
+ (!config.localPatterns?.length ||
+ (config.localPatterns.length === 1 &&
+ config.localPatterns[0].pathname === '/_next/static/media/**'))
+ ) {
+ throw new Error(
+ `Image with src "${src}" is using a query string which is not configured in images.localPatterns.` +
+ `\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns`
+ )
+ }
+
const q = findClosestQuality(quality, config)
return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${q}${
diff --git a/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/layout.tsx b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/layout.tsx
new file mode 100644
index 00000000000000..888614deda3ba5
--- /dev/null
+++ b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/layout.tsx
@@ -0,0 +1,8 @@
+import { ReactNode } from 'react'
+export default function Root({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/page.tsx b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/page.tsx
new file mode 100644
index 00000000000000..ae502dc06f3b17
--- /dev/null
+++ b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/page.tsx
@@ -0,0 +1,6 @@
+import Image from 'next/legacy/image'
+
+export default function Page() {
+ // src with query without localPatterns will throw an error
+ return
+}
diff --git a/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts
new file mode 100644
index 00000000000000..c67f8f06261ac7
--- /dev/null
+++ b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts
@@ -0,0 +1,28 @@
+import { nextTestSetup } from 'e2e-utils'
+
+describe('next-image-legacy-src-with-query-without-local-patterns', () => {
+ const { next, isNextDev, skipped } = nextTestSetup({
+ files: __dirname,
+ skipStart: true,
+ skipDeployment: true,
+ })
+
+ if (skipped) {
+ return
+ }
+
+ it('should throw error for relative image with query without localPatterns for legacy Image', async () => {
+ if (isNextDev) {
+ await next.start()
+ await next.browser('/')
+ expect(next.cliOutput).toContain(
+ 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns'
+ )
+ } else {
+ const { cliOutput } = await next.build()
+ expect(cliOutput).toContain(
+ 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns'
+ )
+ }
+ })
+})
diff --git a/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next.config.js b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next.config.js
new file mode 100644
index 00000000000000..807126e4cf0bf5
--- /dev/null
+++ b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next.config.js
@@ -0,0 +1,6 @@
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {}
+
+module.exports = nextConfig
diff --git a/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/public/test.png b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/public/test.png
new file mode 100644
index 00000000000000..aea7f5ff8ad3e3
Binary files /dev/null and b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/public/test.png differ
diff --git a/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/layout.tsx b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/layout.tsx
new file mode 100644
index 00000000000000..888614deda3ba5
--- /dev/null
+++ b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/layout.tsx
@@ -0,0 +1,8 @@
+import { ReactNode } from 'react'
+export default function Root({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/page.tsx b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/page.tsx
new file mode 100644
index 00000000000000..48b6eeeafd74f3
--- /dev/null
+++ b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/page.tsx
@@ -0,0 +1,6 @@
+import Image from 'next/image'
+
+export default function Page() {
+ // src with query without localPatterns will throw an error
+ return
+}
diff --git a/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts
new file mode 100644
index 00000000000000..4a577b1c5493a5
--- /dev/null
+++ b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts
@@ -0,0 +1,28 @@
+import { nextTestSetup } from 'e2e-utils'
+
+describe('next-image-src-with-query-without-local-patterns', () => {
+ const { next, isNextDev, skipped } = nextTestSetup({
+ files: __dirname,
+ skipStart: true,
+ skipDeployment: true,
+ })
+
+ if (skipped) {
+ return
+ }
+
+ it('should throw error for relative image with query without localPatterns', async () => {
+ if (isNextDev) {
+ await next.start()
+ await next.browser('/')
+ expect(next.cliOutput).toContain(
+ 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns'
+ )
+ } else {
+ const { cliOutput } = await next.build()
+ expect(cliOutput).toContain(
+ 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns'
+ )
+ }
+ })
+})
diff --git a/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next.config.js b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next.config.js
new file mode 100644
index 00000000000000..807126e4cf0bf5
--- /dev/null
+++ b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next.config.js
@@ -0,0 +1,6 @@
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {}
+
+module.exports = nextConfig
diff --git a/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/public/test.png b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/public/test.png
new file mode 100644
index 00000000000000..aea7f5ff8ad3e3
Binary files /dev/null and b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/public/test.png differ
diff --git a/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts
new file mode 100644
index 00000000000000..c67f8f06261ac7
--- /dev/null
+++ b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts
@@ -0,0 +1,28 @@
+import { nextTestSetup } from 'e2e-utils'
+
+describe('next-image-legacy-src-with-query-without-local-patterns', () => {
+ const { next, isNextDev, skipped } = nextTestSetup({
+ files: __dirname,
+ skipStart: true,
+ skipDeployment: true,
+ })
+
+ if (skipped) {
+ return
+ }
+
+ it('should throw error for relative image with query without localPatterns for legacy Image', async () => {
+ if (isNextDev) {
+ await next.start()
+ await next.browser('/')
+ expect(next.cliOutput).toContain(
+ 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns'
+ )
+ } else {
+ const { cliOutput } = await next.build()
+ expect(cliOutput).toContain(
+ 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns'
+ )
+ }
+ })
+})
diff --git a/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next.config.js b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next.config.js
new file mode 100644
index 00000000000000..807126e4cf0bf5
--- /dev/null
+++ b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next.config.js
@@ -0,0 +1,6 @@
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {}
+
+module.exports = nextConfig
diff --git a/test/e2e/next-image-legacy-src-with-query-without-local-patterns/pages/index.tsx b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/pages/index.tsx
new file mode 100644
index 00000000000000..ae502dc06f3b17
--- /dev/null
+++ b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/pages/index.tsx
@@ -0,0 +1,6 @@
+import Image from 'next/legacy/image'
+
+export default function Page() {
+ // src with query without localPatterns will throw an error
+ return
+}
diff --git a/test/e2e/next-image-legacy-src-with-query-without-local-patterns/public/test.png b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/public/test.png
new file mode 100644
index 00000000000000..aea7f5ff8ad3e3
Binary files /dev/null and b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/public/test.png differ
diff --git a/test/e2e/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts b/test/e2e/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts
new file mode 100644
index 00000000000000..4a577b1c5493a5
--- /dev/null
+++ b/test/e2e/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts
@@ -0,0 +1,28 @@
+import { nextTestSetup } from 'e2e-utils'
+
+describe('next-image-src-with-query-without-local-patterns', () => {
+ const { next, isNextDev, skipped } = nextTestSetup({
+ files: __dirname,
+ skipStart: true,
+ skipDeployment: true,
+ })
+
+ if (skipped) {
+ return
+ }
+
+ it('should throw error for relative image with query without localPatterns', async () => {
+ if (isNextDev) {
+ await next.start()
+ await next.browser('/')
+ expect(next.cliOutput).toContain(
+ 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns'
+ )
+ } else {
+ const { cliOutput } = await next.build()
+ expect(cliOutput).toContain(
+ 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns'
+ )
+ }
+ })
+})
diff --git a/test/e2e/next-image-src-with-query-without-local-patterns/next.config.js b/test/e2e/next-image-src-with-query-without-local-patterns/next.config.js
new file mode 100644
index 00000000000000..807126e4cf0bf5
--- /dev/null
+++ b/test/e2e/next-image-src-with-query-without-local-patterns/next.config.js
@@ -0,0 +1,6 @@
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {}
+
+module.exports = nextConfig
diff --git a/test/e2e/next-image-src-with-query-without-local-patterns/pages/index.tsx b/test/e2e/next-image-src-with-query-without-local-patterns/pages/index.tsx
new file mode 100644
index 00000000000000..48b6eeeafd74f3
--- /dev/null
+++ b/test/e2e/next-image-src-with-query-without-local-patterns/pages/index.tsx
@@ -0,0 +1,6 @@
+import Image from 'next/image'
+
+export default function Page() {
+ // src with query without localPatterns will throw an error
+ return
+}
diff --git a/test/e2e/next-image-src-with-query-without-local-patterns/public/test.png b/test/e2e/next-image-src-with-query-without-local-patterns/public/test.png
new file mode 100644
index 00000000000000..aea7f5ff8ad3e3
Binary files /dev/null and b/test/e2e/next-image-src-with-query-without-local-patterns/public/test.png differ
diff --git a/test/unit/next-image-get-img-props.test.ts b/test/unit/next-image-get-img-props.test.ts
index 9069e1b7280e15..e845dd8ca2ecca 100644
--- a/test/unit/next-image-get-img-props.test.ts
+++ b/test/unit/next-image-get-img-props.test.ts
@@ -460,26 +460,6 @@ describe('getImageProps()', () => {
['src', '/test.svg'],
])
})
- it('should auto unoptimized for relative svg with query', async () => {
- const { props } = getImageProps({
- alt: 'a nice desc',
- src: '/test.svg?v=1',
- width: 100,
- height: 200,
- })
- expect(warningMessages).toStrictEqual([
- 'Image with src "/test.svg?v=1" is using a query string which is not configured in images.localPatterns. This config will be required starting in Next.js 16.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns',
- ])
- expect(Object.entries(props)).toStrictEqual([
- ['alt', 'a nice desc'],
- ['loading', 'lazy'],
- ['width', 100],
- ['height', 200],
- ['decoding', 'async'],
- ['style', { color: 'transparent' }],
- ['src', '/test.svg?v=1'],
- ])
- })
it('should auto unoptimized for absolute svg', async () => {
const { props } = getImageProps({
alt: 'a nice desc',