fix: normalize loopback only in hostname (#90158)

`NextURL` intentionally canonicalizes loopback hosts (127.x.x.x, [::1],
localhost) to localhost so that internal URL handling is consistent
across equivalent local origins.

However, we were applying a regex replacement to the entire URL before
parsing, which unintentionally rewrote loopback strings inside query
parameter values, eg `?redirect_uri=http%3A%2F%2F127.0.0.1...`. This is
unexpected for anyone that is explicitly passing through different
loopback values.

This fixes it by first parsing the URL with `new URL()`, and then only
apply the replacement logic to `parsed.hostname` when it is a loopback
host. This leaves search unchanged.

Fixes NEXT-4864
This commit is contained in:
Zack Tanner
2026-02-18 11:54:24 -08:00
committed by GitHub
parent 0d61306201
commit da445e89cd
2 changed files with 19 additions and 5 deletions
+6 -5
View File
@@ -20,13 +20,14 @@ interface Options {
}
const REGEX_LOCALHOST_HOSTNAME =
/(?!^https?:\/\/)(127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|\[::1\]|localhost)/
/^(?:127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|\[::1\]|localhost)$/
function parseURL(url: string | URL, base?: string | URL) {
return new URL(
String(url).replace(REGEX_LOCALHOST_HOSTNAME, 'localhost'),
base && String(base).replace(REGEX_LOCALHOST_HOSTNAME, 'localhost')
)
const parsed = new URL(String(url), base && String(base))
if (REGEX_LOCALHOST_HOSTNAME.test(parsed.hostname)) {
parsed.hostname = 'localhost'
}
return parsed
}
const Internal = Symbol('NextURLInternal')
+13
View File
@@ -205,6 +205,19 @@ it('consider 127.0.0.1 and variations as localhost', () => {
expect(new NextURL('https://[::1]:3000/hello')).toStrictEqual(httpsUrl)
})
it('preserves loopback hostnames inside encoded query parameter values', () => {
const url = new NextURL(
'https://example.com/api/echo?redirect_uri=http%3A%2F%2F127.0.0.1%3A12345%2Ftest'
)
expect(url.searchParams.get('redirect_uri')).toEqual(
'http://127.0.0.1:12345/test'
)
expect(url.href).toEqual(
'https://example.com/api/echo?redirect_uri=http%3A%2F%2F127.0.0.1%3A12345%2Ftest'
)
})
it('allows to change the port', () => {
const url = new NextURL('https://localhost:3000/foo')
url.port = '3001'