Merge commit from fork

https://github.com/sveltejs/svelte/security/advisories/GHSA-f3cj-j4f6-wq85

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/18220/head
Elliott Johnson 2 months ago committed by GitHub
parent d2375e2ebc
commit a16ebc67bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent XSS on `hydratable` from user contents

@ -3,7 +3,6 @@ import { async_mode_flag } from '../flags/index.js';
import { get_render_context } from './render-context.js';
import * as e from './errors.js';
import * as devalue from 'devalue';
import { get_stack } from '../shared/dev.js';
import { DEV } from 'esm-env';
import { get_user_code_location } from './dev.js';
@ -65,7 +64,13 @@ function encode(key, value, unresolved) {
const placeholder = `"${uid++}"`;
const p = value
.then((v) => {
entry.serialized = entry.serialized.replace(placeholder, `r(${uneval(v)})`);
entry.serialized = entry.serialized.replace(
placeholder,
// use the function form here to prevent any string replacement characters from being interpreted
// in `v`, as it's potentially user-controlled and therefore potentially malicious.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement
() => `r(${uneval(v)})`
);
})
.catch((devalue_error) =>
e.hydratable_serialization_failed(

@ -0,0 +1,33 @@
import { afterAll, beforeAll, expect, test } from 'vitest';
import { Renderer } from './renderer.js';
import type { Component } from 'svelte';
import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js';
import { hydratable } from './hydratable.js';
beforeAll(() => {
enable_async_mode_flag();
});
afterAll(() => {
disable_async_mode_flag();
});
test('treats replacement tokens in hydratable promise values as literals', async () => {
const component = (renderer: Renderer) => {
hydratable('key', () => Promise.resolve(`$'`));
renderer.child(async () => {
await Promise.resolve();
});
renderer.push('ok');
};
const { head } = await Renderer.render(component as unknown as Component);
const script_match = head.match(/<script(?:\s[^>]*)?>([\s\S]*)<\/script>/);
expect(script_match, 'expected hydratable script in head output').toBeTruthy();
const script_content = script_match![1];
expect(script_content).toContain('const h = (window.__svelte ??= {}).h ??= new Map();');
expect(script_content).toContain('r("$\'")');
expect(script_content).toMatch(/\[\s*"key"\s*,\s*r\("\$'"\)\s*\]/);
});

@ -29,8 +29,9 @@ polka()
const { head, body } = await render(App);
const html = transformed_template
.replace(`<!--ssr-head-->`, head)
.replace(`<!--ssr-body-->`, body)
// use function form to prevent any string replacement characters from being interpreted
.replace(`<!--ssr-head-->`, () => head)
.replace(`<!--ssr-body-->`, () => body)
// check that Safari doesn't break hydration
.replaceAll('+636-555-3226', '<a href="tel:+636-555-3226">+636-555-3226</a>');

@ -9,8 +9,9 @@ const { head, body } = await render(App);
const rendered = fs
.readFileSync(path.resolve('./dist/client/index.html'), 'utf-8')
.replace(`<!--ssr-body-->`, body)
.replace(`<!--ssr-head-->`, head);
// use function form to prevent any string replacement characters from being interpreted
.replace(`<!--ssr-body-->`, () => body)
.replace(`<!--ssr-head-->`, () => head);
const types = {
'.js': 'application/javascript',

Loading…
Cancel
Save