diff --git a/.changeset/fair-suns-report.md b/.changeset/fair-suns-report.md new file mode 100644 index 000000000000..e4034ae49237 --- /dev/null +++ b/.changeset/fair-suns-report.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: avoid incorrectly un- and re-escaping cookies collected during a server-side `fetch` diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js index e5911209239c..6d0ae7835f03 100644 --- a/packages/kit/src/runtime/server/fetch.js +++ b/packages/kit/src/runtime/server/fetch.js @@ -132,13 +132,16 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade const set_cookie = response.headers.get('set-cookie'); if (set_cookie) { for (const str of set_cookie_parser.splitCookiesString(set_cookie)) { - const { name, value, ...options } = set_cookie_parser.parseString(str); + const { name, value, ...options } = set_cookie_parser.parseString(str, { + decodeValues: false + }); const path = options.path ?? (url.pathname.split('/').slice(0, -1).join('/') || '/'); // options.sameSite is string, something more specific is required - type cast is safe set_internal(name, value, { path, + encode: (value) => value, .../** @type {import('cookie').CookieSerializeOptions} */ (options) }); } diff --git a/packages/kit/test/apps/basics/src/routes/cookies/collect-without-re-escaping/+page.js b/packages/kit/test/apps/basics/src/routes/cookies/collect-without-re-escaping/+page.js new file mode 100644 index 000000000000..8f6f487e6029 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/cookies/collect-without-re-escaping/+page.js @@ -0,0 +1,10 @@ +import { browser } from '$app/environment'; + +/** @type {import('@sveltejs/kit').Load}*/ +export async function load({ fetch }) { + if (!browser) { + // We don't want the client-side collected cookie to clobber the + // server-side collected cookie that we're actually testing. + await fetch('/cookies/collect-without-re-escaping/set-cookie'); + } +} diff --git a/packages/kit/test/apps/basics/src/routes/cookies/collect-without-re-escaping/+page.svelte b/packages/kit/test/apps/basics/src/routes/cookies/collect-without-re-escaping/+page.svelte new file mode 100644 index 000000000000..14ad3ba15ac5 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/cookies/collect-without-re-escaping/+page.svelte @@ -0,0 +1,5 @@ + + +

{browser && document.cookie}

diff --git a/packages/kit/test/apps/basics/src/routes/cookies/collect-without-re-escaping/set-cookie/+server.js b/packages/kit/test/apps/basics/src/routes/cookies/collect-without-re-escaping/set-cookie/+server.js new file mode 100644 index 000000000000..c4a2c1ec28f2 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/cookies/collect-without-re-escaping/set-cookie/+server.js @@ -0,0 +1,4 @@ +/** @type {import('@sveltejs/kit').RequestHandler} */ +export async function GET() { + return new Response(null, { headers: { 'set-cookie': 'cookie-special-characters="foo"' } }); +} diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js index 7bda137157dc..dfa97ce7f3be 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js @@ -827,6 +827,11 @@ test.describe('cookies', () => { await page.locator('button').click(); await expect(page.locator('p')).toHaveText('foo=bar'); }); + + test("fetch during SSR doesn't un- and re-escape cookies", async ({ page }) => { + await page.goto('/cookies/collect-without-re-escaping'); + await expect(page.locator('p')).toHaveText('cookie-special-characters="foo"'); + }); }); test.describe('Interactivity', () => {