From bba9faaa10dffbc738a87c4c0a9db36c2d9d64f5 Mon Sep 17 00:00:00 2001 From: Taku Watabe Date: Mon, 26 Aug 2024 09:42:34 +0900 Subject: [PATCH] bugfix: Remove invalid pathname slashes in `asPathToSearchParams()` --- .../router/utils/as-path-to-search-params.ts | 3 +- .../certificates/localhost-key.pem | 28 ++++++++ .../certificates/localhost.pem | 25 +++++++ .../invalid-url-slash-http.test.ts | 28 ++++++++ .../invalid-url-slash-https.test.ts | 70 +++++++++++++++++++ test/e2e/invalid-url-slash/pages/index.js | 20 ++++++ test/unit/as-path-to-search-params.test.ts | 38 ++++++++++ 7 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 test/e2e/invalid-url-slash/certificates/localhost-key.pem create mode 100644 test/e2e/invalid-url-slash/certificates/localhost.pem create mode 100644 test/e2e/invalid-url-slash/invalid-url-slash-http.test.ts create mode 100644 test/e2e/invalid-url-slash/invalid-url-slash-https.test.ts create mode 100644 test/e2e/invalid-url-slash/pages/index.js create mode 100644 test/unit/as-path-to-search-params.test.ts diff --git a/packages/next/src/shared/lib/router/utils/as-path-to-search-params.ts b/packages/next/src/shared/lib/router/utils/as-path-to-search-params.ts index 0737c14dfdfa7..30aefde5f9a2e 100644 --- a/packages/next/src/shared/lib/router/utils/as-path-to-search-params.ts +++ b/packages/next/src/shared/lib/router/utils/as-path-to-search-params.ts @@ -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 } diff --git a/test/e2e/invalid-url-slash/certificates/localhost-key.pem b/test/e2e/invalid-url-slash/certificates/localhost-key.pem new file mode 100644 index 0000000000000..2306990ac62c5 --- /dev/null +++ b/test/e2e/invalid-url-slash/certificates/localhost-key.pem @@ -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----- diff --git a/test/e2e/invalid-url-slash/certificates/localhost.pem b/test/e2e/invalid-url-slash/certificates/localhost.pem new file mode 100644 index 0000000000000..3d68b42635307 --- /dev/null +++ b/test/e2e/invalid-url-slash/certificates/localhost.pem @@ -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----- diff --git a/test/e2e/invalid-url-slash/invalid-url-slash-http.test.ts b/test/e2e/invalid-url-slash/invalid-url-slash-http.test.ts new file mode 100644 index 0000000000000..105c15a57fb61 --- /dev/null +++ b/test/e2e/invalid-url-slash/invalid-url-slash-http.test.ts @@ -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') + }) +}) diff --git a/test/e2e/invalid-url-slash/invalid-url-slash-https.test.ts b/test/e2e/invalid-url-slash/invalid-url-slash-https.test.ts new file mode 100644 index 0000000000000..2c55dec4b3e2f --- /dev/null +++ b/test/e2e/invalid-url-slash/invalid-url-slash-https.test.ts @@ -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((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') + }) +}) diff --git a/test/e2e/invalid-url-slash/pages/index.js b/test/e2e/invalid-url-slash/pages/index.js new file mode 100644 index 0000000000000..ef36405fa673f --- /dev/null +++ b/test/e2e/invalid-url-slash/pages/index.js @@ -0,0 +1,20 @@ +'use client' + +export default function Page() { + const goInvalidUrl = () => { + const { protocol, hostname, port } = location + + location.href = `${protocol}//${hostname}${port ? ':' + port : ''}//` + } + + return ( + <> +

index page

+

+ +

+ + ) +} diff --git a/test/unit/as-path-to-search-params.test.ts b/test/unit/as-path-to-search-params.test.ts new file mode 100644 index 0000000000000..5e65472f62fad --- /dev/null +++ b/test/unit/as-path-to-search-params.test.ts @@ -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' + ) + }) +})