CVE-2026-40299
Description
next-intl provides internationalization for Next.js. Applications using the next-intl middleware prior to version 4.9.1with localePrefix: 'as-needed' could construct URLs where path handling and the WHATWG URL parser resolved a relative redirect target to another host (e.g. scheme-relative // or control characters stripped by the URL parser), so the middleware could redirect the browser off-site while the user still started from a trusted app URL. The problem has been patchedin next-intl@4.9.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
next-intlnpm | < 4.9.1 | 4.9.1 |
Affected products
1Patches
11c80b668aa6dfix: Improve middleware pathname validation (#2304)
4 files changed · +94 −5
packages/next-intl/.size-limit.ts+1 −1 modified@@ -42,7 +42,7 @@ const config: SizeLimitConfig = [ { name: "import * from 'next-intl/middleware'", path: 'dist/esm/production/middleware.js', - limit: '10.1 KB' + limit: '10.12 KB' }, { name: "import * from 'next-intl/routing'",
packages/next-intl/src/middleware/middleware.test.tsx+38 −0 modified@@ -219,6 +219,44 @@ describe('prefix-based routing', () => { ); }); + describe('open redirect prevention', () => { + it('redirects to a same-origin URL when the path contains a TAB after decodeURI', () => { + middleware(createMockRequest('/en/\t/example.org')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/example.org' + ); + }); + + it('redirects to a same-origin URL when the path contains an encoded backslash', () => { + middleware(createMockRequest('/en/%5Cexample.org')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/%5Cexample.org' + ); + }); + + it('redirects to a same-origin URL when the path contains excess slashes before a segment', () => { + middleware(createMockRequest('/en///example.org')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/example.org' + ); + }); + + it('redirects to a same-origin URL when TAB is double-encoded as %2509', () => { + middleware(createMockRequest('/en/%2509/some-page')); + expect(MockedNextResponse.next).not.toHaveBeenCalled(); + expect(MockedNextResponse.rewrite).not.toHaveBeenCalled(); + expect(MockedNextResponse.redirect.mock.calls[0][0].toString()).toBe( + 'http://localhost:3000/%09/some-page' + ); + }); + }); + it('redirects requests for the default locale when prefixed at sub paths', () => { middleware(createMockRequest('/en/about')); expect(MockedNextResponse.next).not.toHaveBeenCalled();
packages/next-intl/src/middleware/utils.test.tsx+41 −1 modified@@ -4,9 +4,49 @@ import { getInternalTemplate, getNormalizedPathname, getPathnameMatch, - getRouteParams + getRouteParams, + sanitizePathname } from './utils.js'; +describe('sanitizePathname', () => { + it('leaves normal pathnames unchanged', () => { + expect(sanitizePathname('/en/about')).toBe('/en/about'); + expect(sanitizePathname('/ja/%E7%B4%84')).toBe('/ja/%E7%B4%84'); + expect(sanitizePathname('/')).toBe('/'); + }); + + it('encodes backslashes to prevent scheme-relative redirect via \\host', () => { + expect(sanitizePathname('/en/\\example.org')).toBe('/en/%5Cexample.org'); + expect(sanitizePathname('/en/%5Cexample.org')).toBe('/en/%5Cexample.org'); + }); + + it('collapses consecutive slashes to prevent scheme-relative redirect via //host', () => { + expect(sanitizePathname('/en////example.org')).toBe('/en/example.org'); + expect(sanitizePathname('//example.org')).toBe('/example.org'); + }); + + it('strips TAB (U+0009) to prevent //host collapse', () => { + expect(sanitizePathname('/en/\t/example.org')).toBe('/en/example.org'); + expect(sanitizePathname('\t//example.org')).toBe('/example.org'); + }); + + it('strips LF (U+000A)', () => { + expect(sanitizePathname('/en/\n/example.org')).toBe('/en/example.org'); + }); + + it('strips CR (U+000D)', () => { + expect(sanitizePathname('/en/\r/example.org')).toBe('/en/example.org'); + }); + + it('strips multiple whitespace characters in combination', () => { + expect(sanitizePathname('/en/\t\r\n/example.org')).toBe('/en/example.org'); + }); + + it('applies replacements in the correct order: backslash before slash collapse', () => { + expect(sanitizePathname('/en\\/example.org')).toBe('/en%5C/example.org'); + }); +}); + describe('getNormalizedPathname', () => { it('should return the normalized pathname', () => { function getResult(pathname: string) {
packages/next-intl/src/middleware/utils.tsx+14 −3 modified@@ -322,7 +322,18 @@ export function getLocaleAsPrefix<AppLocales extends Locales>( export function sanitizePathname(pathname: string) { // Sanitize malicious URIs, e.g.: - // '/en/\\example.org → /en/%5C%5Cexample.org' - // '/en////example.org → /en/example.org' - return pathname.replace(/\\/g, '%5C').replace(/\/+/g, '/'); + // '/en/\\example.org' → '/en/%5Cexample.org' (backslash → %5C) + // '/en/\t/example.org' → '/en/example.org' (WHATWG-stripped TAB) + // '/en/\n/example.org' → '/en/example.org' (WHATWG-stripped LF) + // '/en/\r/example.org' → '/en/example.org' (WHATWG-stripped CR) + // '/en////example.org' → '/en/example.org' (consecutive slashes) + // + // U+0009/000A/000D are silently stripped by the WHATWG URL parser + // (https://url.spec.whatwg.org/#concept-url-parser). Without removing + // them here, a decoded TAB in a segment separator position causes + // new URL("/\t/host", base) to collapse to "//host" → open redirect. + return pathname + .replace(/\\/g, '%5C') + .replace(/[\t\n\r]/g, '') + .replace(/\/+/g, '/'); }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-8f24-v5vv-gm5jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-40299ghsaADVISORY
- github.com/amannn/next-intl/commit/1c80b668aa6d853f470319eec10a3f61e78a70e6nvdWEB
- github.com/amannn/next-intl/pull/2304nvdWEB
- github.com/amannn/next-intl/releases/tag/v4.9.1nvdWEB
- github.com/amannn/next-intl/security/advisories/GHSA-8f24-v5vv-gm5jnvdWEB
News mentions
0No linked articles in our index yet.