diff --git a/.changeset/curly-ants-guess.md b/.changeset/curly-ants-guess.md new file mode 100644 index 000000000000..9b2911fefa3c --- /dev/null +++ b/.changeset/curly-ants-guess.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[fix] prevent double decoding of params diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index b622af0c4824..7877024ee85b 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1,5 +1,11 @@ import { onMount, tick } from 'svelte'; -import { make_trackable, decode_params, normalize_path, add_data_suffix } from '../../utils/url.js'; +import { + make_trackable, + decode_pathname, + decode_params, + normalize_path, + add_data_suffix +} from '../../utils/url.js'; import { find_anchor, get_base_uri, scroll_state } from './utils.js'; import { lock_fetch, @@ -992,7 +998,7 @@ export function create_client({ target, base, trailing_slash }) { function get_navigation_intent(url, invalidating) { if (is_external_url(url)) return; - const path = decodeURI(url.pathname.slice(base.length) || '/'); + const path = decode_pathname(url.pathname.slice(base.length) || '/'); for (const route of routes) { const params = route.exec(path); diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 0b479fc60a97..aa52e2860d55 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -6,6 +6,7 @@ import { coalesce_to_error } from '../../utils/error.js'; import { is_form_content_type } from '../../utils/http.js'; import { GENERIC_ERROR, handle_fatal_error } from './utils.js'; import { + decode_pathname, decode_params, disable_search, has_data_suffix, @@ -44,7 +45,7 @@ export async function respond(request, options, state) { let decoded; try { - decoded = decodeURI(url.pathname); + decoded = decode_pathname(url.pathname); } catch { return new Response('Malformed URI', { status: 400 }); } diff --git a/packages/kit/src/utils/url.js b/packages/kit/src/utils/url.js index 77d60b789c16..db4e6d882137 100644 --- a/packages/kit/src/utils/url.js +++ b/packages/kit/src/utils/url.js @@ -54,23 +54,20 @@ export function normalize_path(path, trailing_slash) { return path; } +/** + * Decode pathname excluding %25 to prevent further double decoding of params + * @param {string} pathname + */ +export function decode_pathname(pathname) { + return pathname.split('%25').map(decodeURI).join('%25'); +} + /** @param {Record} params */ export function decode_params(params) { for (const key in params) { // input has already been decoded by decodeURI - // now handle the rest that decodeURIComponent would do - params[key] = params[key] - .replace(/%23/g, '#') - .replace(/%3[Bb]/g, ';') - .replace(/%2[Cc]/g, ',') - .replace(/%2[Ff]/g, '/') - .replace(/%3[Ff]/g, '?') - .replace(/%3[Aa]/g, ':') - .replace(/%40/g, '@') - .replace(/%26/g, '&') - .replace(/%3[Dd]/g, '=') - .replace(/%2[Bb]/g, '+') - .replace(/%24/g, '$'); + // now handle the rest + params[key] = decodeURIComponent(params[key]); } return params; diff --git a/packages/kit/test/apps/basics/src/routes/encoded/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/+page.svelte index b21e07113fa9..3b96b33a5ef4 100644 --- a/packages/kit/test/apps/basics/src/routes/encoded/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/encoded/+page.svelte @@ -5,5 +5,6 @@ @svelte $SVLT test%20me +test%2fme AC/DC [ diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 7c7632120788..5b18cfa246a4 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -331,6 +331,14 @@ test.describe('Encoded paths', () => { expect(await page.innerHTML('h3')).toBe('/encoded/test%2520me: test%20me'); }); + test('visits a route with a doubly encoded slash', async ({ page, clicknav }) => { + await page.goto('/encoded'); + await clicknav('[href="/encoded/test%252fme"]'); + expect(await page.innerHTML('h1')).toBe('dynamic'); + expect(await page.innerHTML('h2')).toBe('/encoded/test%252fme: test%2fme'); + expect(await page.innerHTML('h3')).toBe('/encoded/test%252fme: test%2fme'); + }); + test('visits a route with an encoded slash', async ({ page, clicknav }) => { await page.goto('/encoded'); await clicknav('[href="/encoded/AC%2fDC"]');