Skip to content

Commit d338d46

Browse files
authored
fix: escape values included in dev 404 page (#13039)
1 parent 5f8399d commit d338d46

File tree

7 files changed

+69
-40
lines changed

7 files changed

+69
-40
lines changed

.changeset/five-maps-yawn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: escape values included in dev 404 page

packages/kit/src/core/postbuild/prerender.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
44
import { installPolyfills } from '../../exports/node/polyfills.js';
55
import { mkdirp, posixify, walk } from '../../utils/filesystem.js';
66
import { decode_uri, is_root_relative, resolve } from '../../utils/url.js';
7-
import { escape_html_attr } from '../../utils/escape.js';
7+
import { escape_html } from '../../utils/escape.js';
88
import { logger } from '../utils.js';
99
import { load_config } from '../config/index.js';
1010
import { get_route_segments } from '../../utils/routing.js';
@@ -359,9 +359,10 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
359359
dest,
360360
`<script>location.href=${devalue.uneval(
361361
location
362-
)};</script><meta http-equiv="refresh" content=${escape_html_attr(
363-
`0;url=${location}`
364-
)}>`
362+
)};</script><meta http-equiv="refresh" content="${escape_html(
363+
`0;url=${location}`,
364+
true
365+
)}">`
365366
);
366367

367368
written.add(file);

packages/kit/src/exports/vite/utils.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { loadEnv } from 'vite';
33
import { posixify } from '../../utils/filesystem.js';
44
import { negotiate } from '../../utils/http.js';
55
import { filter_private_env, filter_public_env } from '../../utils/env.js';
6+
import { escape_html } from '../../utils/escape.js';
67

78
/**
89
* Transforms kit.alias to a valid vite.resolve.alias array.
@@ -89,11 +90,17 @@ export function not_found(req, res, base) {
8990
if (type === 'text/html') {
9091
res.setHeader('Content-Type', 'text/html');
9192
res.end(
92-
`The server is configured with a public base URL of ${base} - did you mean to visit <a href="${prefixed}">${prefixed}</a> instead?`
93+
`The server is configured with a public base URL of ${escape_html(
94+
base
95+
)} - did you mean to visit <a href="${escape_html(prefixed, true)}">${escape_html(
96+
prefixed
97+
)}</a> instead?`
9398
);
9499
} else {
95100
res.end(
96-
`The server is configured with a public base URL of ${base} - did you mean to visit ${prefixed} instead?`
101+
`The server is configured with a public base URL of ${escape_html(
102+
base
103+
)} - did you mean to visit ${escape_html(prefixed)} instead?`
97104
);
98105
}
99106
}

packages/kit/src/runtime/server/page/csp.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { escape_html_attr } from '../../../utils/escape.js';
1+
import { escape_html } from '../../../utils/escape.js';
22
import { base64, sha256 } from './crypto.js';
33

44
const array = new Uint8Array(16);
@@ -300,7 +300,7 @@ class CspProvider extends BaseProvider {
300300
return;
301301
}
302302

303-
return `<meta http-equiv="content-security-policy" content=${escape_html_attr(content)}>`;
303+
return `<meta http-equiv="content-security-policy" content="${escape_html(content, true)}">`;
304304
}
305305
}
306306

packages/kit/src/runtime/server/page/serialize_data.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { escape_html_attr } from '../../../utils/escape.js';
1+
import { escape_html } from '../../../utils/escape.js';
22
import { hash } from '../../hash.js';
33

44
/**
@@ -70,7 +70,7 @@ export function serialize_data(fetched, filter, prerendering = false) {
7070
const attrs = [
7171
'type="application/json"',
7272
'data-sveltekit-fetched',
73-
`data-url=${escape_html_attr(fetched.url)}`
73+
`data-url="${escape_html(fetched.url, true)}"`
7474
];
7575

7676
if (fetched.is_b64) {

packages/kit/src/utils/escape.js

+36-20
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,57 @@
66
const escape_html_attr_dict = {
77
'&': '&amp;',
88
'"': '&quot;'
9+
// Svelte also escapes < because the escape function could be called inside a `noscript` there
10+
// https://github.com/sveltejs/svelte/security/advisories/GHSA-8266-84wp-wv5c
11+
// However, that doesn't apply in SvelteKit
912
};
1013

14+
/**
15+
* @type {Record<string, string>}
16+
*/
17+
const escape_html_dict = {
18+
'&': '&amp;',
19+
'<': '&lt;'
20+
};
21+
22+
const surrogates = // high surrogate without paired low surrogate
23+
'[\\ud800-\\udbff](?![\\udc00-\\udfff])|' +
24+
// a valid surrogate pair, the only match with 2 code units
25+
// we match it so that we can match unpaired low surrogates in the same pass
26+
// TODO: use lookbehind assertions once they are widely supported: (?<![\ud800-udbff])[\udc00-\udfff]
27+
'[\\ud800-\\udbff][\\udc00-\\udfff]|' +
28+
// unpaired low surrogate (see previous match)
29+
'[\\udc00-\\udfff]';
30+
1131
const escape_html_attr_regex = new RegExp(
12-
// special characters
13-
`[${Object.keys(escape_html_attr_dict).join('')}]|` +
14-
// high surrogate without paired low surrogate
15-
'[\\ud800-\\udbff](?![\\udc00-\\udfff])|' +
16-
// a valid surrogate pair, the only match with 2 code units
17-
// we match it so that we can match unpaired low surrogates in the same pass
18-
// TODO: use lookbehind assertions once they are widely supported: (?<![\ud800-udbff])[\udc00-\udfff]
19-
'[\\ud800-\\udbff][\\udc00-\\udfff]|' +
20-
// unpaired low surrogate (see previous match)
21-
'[\\udc00-\\udfff]',
32+
`[${Object.keys(escape_html_attr_dict).join('')}]|` + surrogates,
33+
'g'
34+
);
35+
36+
const escape_html_regex = new RegExp(
37+
`[${Object.keys(escape_html_dict).join('')}]|` + surrogates,
2238
'g'
2339
);
2440

2541
/**
26-
* Formats a string to be used as an attribute's value in raw HTML.
27-
*
28-
* It escapes unpaired surrogates (which are allowed in js strings but invalid in HTML), escapes
29-
* characters that are special in attributes, and surrounds the whole string in double-quotes.
42+
* Escapes unpaired surrogates (which are allowed in js strings but invalid in HTML) and
43+
* escapes characters that are special.
3044
*
3145
* @param {string} str
32-
* @returns {string} Escaped string surrounded by double-quotes.
33-
* @example const html = `<tag data-value=${escape_html_attr('value')}>...</tag>`;
46+
* @param {boolean} [is_attr]
47+
* @returns {string} escaped string
48+
* @example const html = `<tag data-value="${escape_html('value', true)}">...</tag>`;
3449
*/
35-
export function escape_html_attr(str) {
36-
const escaped_str = str.replace(escape_html_attr_regex, (match) => {
50+
export function escape_html(str, is_attr) {
51+
const dict = is_attr ? escape_html_attr_dict : escape_html_dict;
52+
const escaped_str = str.replace(is_attr ? escape_html_attr_regex : escape_html_regex, (match) => {
3753
if (match.length === 2) {
3854
// valid surrogate pair
3955
return match;
4056
}
4157

42-
return escape_html_attr_dict[match] ?? `&#${match.charCodeAt(0)};`;
58+
return dict[match] ?? `&#${match.charCodeAt(0)};`;
4359
});
4460

45-
return `"${escaped_str}"`;
61+
return escaped_str;
4662
}

packages/kit/src/utils/escape.spec.js

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { assert, test } from 'vitest';
2-
import { escape_html_attr } from './escape.js';
2+
import { escape_html } from './escape.js';
33

44
test('escape_html_attr escapes special attribute characters', () => {
55
assert.equal(
6-
escape_html_attr('some "values" are &special here, <others> aren\'t.'),
7-
'"some &quot;values&quot; are &amp;special here, <others> aren\'t."'
6+
escape_html('some "values" are &special here, <others> aren\'t.', true),
7+
"some &quot;values&quot; are &amp;special here, <others> aren't."
88
);
99
});
1010

1111
test('escape_html_attr escapes invalid surrogates', () => {
12-
assert.equal(escape_html_attr('\ud800\udc00'), '"\ud800\udc00"');
13-
assert.equal(escape_html_attr('\ud800'), '"&#55296;"');
14-
assert.equal(escape_html_attr('\udc00'), '"&#56320;"');
15-
assert.equal(escape_html_attr('\udc00\ud800'), '"&#56320;&#55296;"');
16-
assert.equal(escape_html_attr('\ud800\ud800\udc00'), '"&#55296;\ud800\udc00"');
17-
assert.equal(escape_html_attr('\ud800\udc00\udc00'), '"\ud800\udc00&#56320;"');
18-
assert.equal(escape_html_attr('\ud800\ud800\udc00\udc00'), '"&#55296;\ud800\udc00&#56320;"');
12+
assert.equal(escape_html('\ud800\udc00', true), '\ud800\udc00');
13+
assert.equal(escape_html('\ud800', true), '&#55296;');
14+
assert.equal(escape_html('\udc00', true), '&#56320;');
15+
assert.equal(escape_html('\udc00\ud800', true), '&#56320;&#55296;');
16+
assert.equal(escape_html('\ud800\ud800\udc00', true), '&#55296;\ud800\udc00');
17+
assert.equal(escape_html('\ud800\udc00\udc00', true), '\ud800\udc00&#56320;');
18+
assert.equal(escape_html('\ud800\ud800\udc00\udc00', true), '&#55296;\ud800\udc00&#56320;');
1919
});

0 commit comments

Comments
 (0)