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 test +} 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 test +} 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 test +} 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 test +} 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',