Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't throw errors when we give "asPathToSearchParams()" an invalid path with multiple leading slashes #70144

Open
wants to merge 1 commit into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Convert router.asPath to a URLSearchParams object
// example: /dynamic/[slug]?foo=bar -> { foo: 'bar' }
export function asPathToSearchParams(asPath: string): URLSearchParams {
return new URL(asPath, 'http://n').searchParams
const asPathWithoutLeadingSlashes = asPath.replace(/^\/{2,}/, '/')
return new URL(asPathWithoutLeadingSlashes, 'http://n').searchParams
}
28 changes: 28 additions & 0 deletions test/e2e/invalid-url-slash/certificates/localhost-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC2skb3RwQBKtJL
kLfxmPMxAZG4o6R4m9ipcMJtpB6im6di7g+fZzRP+j0AtOnnIaM6ZuT61nCTZKd7
RB/wlrhgEBkIqEGJtJh93Z0An3J2tt7YnBKsJNBP5BqqLD2TDyFGgTjfKdXJObSr
t67IiRtrKdT6VgRtM2gTM+d1lvL5IVc9JiTOeK9QjzVmDI2MIprzkFrQPI7G+UDq
P80IlrRX2mj30g+rIg6ma6GcnG0Wrk0csaghuAQSiLUqoGU3TzG7QK95sMS48xrv
0tpIyIWlWTTr17ntOIWA1cAGKUQadeVc2No/VRJbMxIcjmtDUc1e66c46z/KTgHo
2vSzjWVNAgMBAAECggEACuIT2Cci1e73GAlG691wnzq4s4cMBSNDhNRywJVGPemH
zxzfUV+Ufi8p8yDTzjDyyEfY3BhqHF2inHUyceKImTBcTWe4f7uCWf0ZnS/iYbAD
FmQ1uIt43Ul5TSnVgS0ljk2kVaboVVRaruACSW/hckDLrx3wpZCqYnp1D0wurSh1
hQJsb2Opy5wve+hEUGr8AYlolB2SyQLtdWkwyNTmtDeb/cIM/NKXuWDECBrgEN8y
v6fluRe4YkLqzTOJvMr4xNtA9wshO8qcNWy6T5YaiNRJJmzBq2V3d41UuYRcih2K
jSRfn9xZvGPWZ92+Jce15pZY5+ec9A1PuT6J8304AQKBgQDbyWoPZxE6yEXDWz4+
eFEvPGlVR8KiZhJl78iC1b9gW+FNlmd/LlamRB21IGfpoJgoRyjFP6UJ/tqGx79x
c+7krcBFCOfckYhRGCLxFdK2O0FTQ8ltfqaSYG4jeTSPVDaxhMYh/H3xjls9i7Y7
90sWDjE/uSFuT+ExmNZvvVtjdQKBgQDUzGToK5gqFpVO43pcJbM6odBTVbZSxDMF
cq+KEOT/c7CzKOOQj7UTqiQgXM0odiJ/I4ZvaIlv+s7l/Cr9LyII3gZUQdPnszsM
/N8kMeJC5Oy8O5pboo+p4SOosRTVZhxGB1tTQxqyECZV5Y5ZCH6bGV4cEqO6rjYC
Pkunc6B3eQKBgCLmtg/iFwtVmDZwe87hvkqY9kUTkyXEvbEwRY/5L1222W0/sAmz
KxFWCb2kervPw7nJqwC/nY6byMnUWGNEvK/Vo42S33bYKWRvR8Uu6PoFKNd3ETpw
/TSLWZIKgj0sa07/PZNSDBHawERitjqJh4PmFw3+cP+acbE1iv/NewCtAoGAWOzt
QiRtmzECxgvDp1xN0LOsNhb8cQvyclVhy+WRfLrg3Y25w0B6oDQakreVOFJdyhmT
ZV0fCf+alHtTj6gxpdj6dh1oK0w34g6ORTbfYar+zw5tS9vcA1bFKwqNNTxNlmoe
nOXO8xhSnNSoLsag+bmZHUwgxbNleHyF6v0j0qkCgYAklO2RDc2RDccXYAe6MbT7
EYExvS+K09CF4PVPoFk1QZxENuXfewDli9UCwt+uJvVacJOnUyLjMkkooibTTk+A
xf/dAp5ECrTHny/cMSerFJEgVZYsH06m+0a+RP+zL45skm/EsidOvIS80OkRr+Vz
rgIUqg2F14h2H2vbBlqrqQ==
-----END PRIVATE KEY-----
25 changes: 25 additions & 0 deletions test/e2e/invalid-url-slash/certificates/localhost.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEKDCCApCgAwIBAgIRAOCKMC740uI5i7wASQ65kkgwDQYJKoZIhvcNAQELBQAw
bTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSEwHwYDVQQLDBhqakBz
bGF0ZS5sYW4gKEpKIEthc3BlcikxKDAmBgNVBAMMH21rY2VydCBqakBzbGF0ZS5s
YW4gKEpKIEthc3BlcikwHhcNMjMwMTI2MDQyMjA2WhcNMjUwNDI2MDMyMjA2WjBM
MScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxITAfBgNV
BAsMGGpqQHNsYXRlLmxhbiAoSkogS2FzcGVyKTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBALayRvdHBAEq0kuQt/GY8zEBkbijpHib2Klwwm2kHqKbp2Lu
D59nNE/6PQC06echozpm5PrWcJNkp3tEH/CWuGAQGQioQYm0mH3dnQCfcna23tic
Eqwk0E/kGqosPZMPIUaBON8p1ck5tKu3rsiJG2sp1PpWBG0zaBMz53WW8vkhVz0m
JM54r1CPNWYMjYwimvOQWtA8jsb5QOo/zQiWtFfaaPfSD6siDqZroZycbRauTRyx
qCG4BBKItSqgZTdPMbtAr3mwxLjzGu/S2kjIhaVZNOvXue04hYDVwAYpRBp15VzY
2j9VElszEhyOa0NRzV7rpzjrP8pOAeja9LONZU0CAwEAAaNkMGIwDgYDVR0PAQH/
BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFOCz+XaB1lpu
T6P2UIfjbq7pUX9GMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG
9w0BAQsFAAOCAYEAaIPJdegZqcKys25yF5MYV10OyZ4wpP4gkONaA5oLfE/SeasH
cs1Af85oeGh6fCuszAoDCJHYvSsUqGhg/243Dkl993vGeLVikvC4R/LC/IkiwNKd
91byKnQBDghzeynyJiHO9yEiY4VFxUTThbttfDstDePOxHYxt8bLbIpGNZb0fdeh
opDwD9hhfL90A7sN7MdakBfQajpquyQgWvKGedaQx363WVXkssi4xRAIdm77ZzqA
Cy4WVrnRtvSbJNqYtNT0Ibx3gF7WC+ACh13lnniDjQlfcDJWxOVjYyvPx0k4M0KQ
+Qz/sx4RXy5jI9OO/hxJNNc3HAxU/7fLtnO/VtEb/c9A1m5gPUFU0W/EHL5VPJJ6
A6+/T8wK8hDEY4j+bMrysbOeTrbTyLmFXMGFkkSI7OjX0RHkgYBJclgBZfQYGrh1
PDmdZ26GSgt39k6VCY6ur7dXQuMPvQmRM6IqiPQpmd9SYP+FEiJh5TAOUBinI/Cu
hvseiJ2JQx+4YqxB
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions test/e2e/invalid-url-slash/invalid-url-slash-http.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { join } from 'node:path'
import webdriver from 'next-webdriver'
import { createNext, FileRef, type NextInstance } from 'e2e-utils'

// Pattern that does not cause error
describe('invalid HTTP URL slash', () => {
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: {
pages: new FileRef(join(__dirname, 'pages')),
},
})
})

afterAll(() => {
next.destroy()
})

it('should navigate "//" correctly client-side', async () => {
const browser = await webdriver(next.url, '/')
// Change to "//"
await browser.elementByCss('button').click().waitForIdleNetwork()
const text = await browser.waitForElementByCss('h1', 100).text()
expect(text).toBe('index page')
})
})
70 changes: 70 additions & 0 deletions test/e2e/invalid-url-slash/invalid-url-slash-https.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import fs from 'node:fs'
import https from 'node:https'
import { join } from 'node:path'
import httpProxy from 'http-proxy'
import { findPort } from 'next-test-utils'
import webdriver from 'next-webdriver'
import { FileRef, nextTestSetup } from 'e2e-utils'

// Pattern that may cause error
describe('invalid HTTPS URL slash', () => {
let proxyPort: number
let proxyServer: https.Server

const { next, skipped } = nextTestSetup({
files: {
pages: new FileRef(join(__dirname, 'pages')),
},
// This test is skipped when deployed because it relies on a proxy server
skipDeployment: true,
})

if (skipped) return

beforeAll(async () => {
proxyPort = await findPort()

const ssl = {
key: fs.readFileSync(
join(__dirname, 'certificates/localhost-key.pem'),
'utf8'
),
cert: fs.readFileSync(
join(__dirname, 'certificates/localhost.pem'),
'utf8'
),
}

const proxy = httpProxy.createProxyServer({
target: `http://localhost:${next.appPort}`,
ssl,
secure: false,
})

proxyServer = https.createServer(ssl, async (req, res) => {
proxy.web(req, res)
})

proxy.on('error', (err) => {
throw new Error(`Failed to proxy: ${err.message}`)
})

await new Promise<void>((resolve) => {
proxyServer.listen(proxyPort, () => resolve())
})
})

afterAll(() => {
proxyServer.close()
})

it('should navigate "//" correctly client-side', async () => {
const browser = await webdriver(`https://localhost:${proxyPort}`, '/', {
ignoreHTTPSErrors: true,
})
// Change to "//"
await browser.elementByCss('button').click().waitForIdleNetwork()
const text = await browser.waitForElementByCss('h1', 100).text()
expect(text).toBe('index page')
})
})
20 changes: 20 additions & 0 deletions test/e2e/invalid-url-slash/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client'

export default function Page() {
const goInvalidUrl = () => {
const { protocol, hostname, port } = location

location.href = `${protocol}//${hostname}${port ? ':' + port : ''}//`
}

return (
<>
<h1>index page</h1>
<p>
<button type="button" onClick={goInvalidUrl}>
Go invalid URL
</button>
</p>
</>
)
}
38 changes: 38 additions & 0 deletions test/unit/as-path-to-search-params.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint-env jest */
import { asPathToSearchParams } from 'next/dist/shared/lib/router/utils/as-path-to-search-params'

describe('asPathToSearchParams', () => {
// Convenience function so tests can be aligned neatly
// and easy to eyeball
const check = (asPath: string, queries: string) => {
const searchParams = asPathToSearchParams(asPath)
const correctSearchParams = new URLSearchParams(queries)

Array.from(correctSearchParams.keys()).forEach((key) => {
expect(searchParams.has(key)).toBe(true)
expect(searchParams.get(key)).toStrictEqual(correctSearchParams.get(key))
})
}

it('should get valid root relative path', () => {
check('/', '')
})

it('should get converted valid root relative path from invalid URL', () => {
check('//', '')
})

it('should get valid relative path', () => {
check(
'/pathA/pathB?fooC=barD&fooE=barF&fooE=barG#hashH',
'fooC=barD&fooE=barF&fooE=barG'
)
})

it('should get converted valid relative path from invalid URL', () => {
check(
'//pathA/pathB?fooC=barD&fooE=barF&fooE=barG#hashH',
'fooC=barD&fooE=barF&fooE=barG'
)
})
})
Loading