VYPR
Low severityNVD Advisory· Published Nov 25, 2024· Updated Nov 25, 2024

Cross-Site Scripting attack (XSS) on dev mode 404 page in SvelteKit

CVE-2024-53261

Description

SvelteKit is a framework for rapidly developing robust, performant web applications using Svelte. "Unsanitized input from *the request URL* flows into end, where it is used to render an HTML page returned to the user. This may result in a Cross-Site Scripting attack (XSS)." The files packages/kit/src/exports/vite/dev/index.js and packages/kit/src/exports/vite/utils.js both contain user controllable data which under specific conditions may flow to dev mode pages. There is little to no expected impact. The Vite development is not exposed to the network by default and even if someone were able to trick a developer into executing an XSS against themselves, a development database should not have any sensitive data. None the less this issue has been addressed in version 2.8.3 and all users are advised to upgrade.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@sveltejs/kitnpm
< 2.8.32.8.3

Affected products

1

Patches

1
d338d4635a7f

fix: escape values included in dev 404 page (#13039)

https://github.com/sveltejs/kitBen McCannNov 25, 2024via ghsa
7 files changed · +69 40
  • .changeset/five-maps-yawn.md+5 0 added
    @@ -0,0 +1,5 @@
    +---
    +'@sveltejs/kit': patch
    +---
    +
    +fix: escape values included in dev 404 page
    
  • packages/kit/src/core/postbuild/prerender.js+5 4 modified
    @@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url';
     import { installPolyfills } from '../../exports/node/polyfills.js';
     import { mkdirp, posixify, walk } from '../../utils/filesystem.js';
     import { decode_uri, is_root_relative, resolve } from '../../utils/url.js';
    -import { escape_html_attr } from '../../utils/escape.js';
    +import { escape_html } from '../../utils/escape.js';
     import { logger } from '../utils.js';
     import { load_config } from '../config/index.js';
     import { get_route_segments } from '../../utils/routing.js';
    @@ -359,9 +359,10 @@ async function prerender({ out, manifest_path, metadata, verbose, env }) {
     						dest,
     						`<script>location.href=${devalue.uneval(
     							location
    -						)};</script><meta http-equiv="refresh" content=${escape_html_attr(
    -							`0;url=${location}`
    -						)}>`
    +						)};</script><meta http-equiv="refresh" content="${escape_html(
    +							`0;url=${location}`,
    +							true
    +						)}">`
     					);
     
     					written.add(file);
    
  • packages/kit/src/exports/vite/utils.js+9 2 modified
    @@ -3,6 +3,7 @@ import { loadEnv } from 'vite';
     import { posixify } from '../../utils/filesystem.js';
     import { negotiate } from '../../utils/http.js';
     import { filter_private_env, filter_public_env } from '../../utils/env.js';
    +import { escape_html } from '../../utils/escape.js';
     
     /**
      * Transforms kit.alias to a valid vite.resolve.alias array.
    @@ -89,11 +90,17 @@ export function not_found(req, res, base) {
     	if (type === 'text/html') {
     		res.setHeader('Content-Type', 'text/html');
     		res.end(
    -			`The server is configured with a public base URL of ${base} - did you mean to visit <a href="${prefixed}">${prefixed}</a> instead?`
    +			`The server is configured with a public base URL of ${escape_html(
    +				base
    +			)} - did you mean to visit <a href="${escape_html(prefixed, true)}">${escape_html(
    +				prefixed
    +			)}</a> instead?`
     		);
     	} else {
     		res.end(
    -			`The server is configured with a public base URL of ${base} - did you mean to visit ${prefixed} instead?`
    +			`The server is configured with a public base URL of ${escape_html(
    +				base
    +			)} - did you mean to visit ${escape_html(prefixed)} instead?`
     		);
     	}
     }
    
  • packages/kit/src/runtime/server/page/csp.js+2 2 modified
    @@ -1,4 +1,4 @@
    -import { escape_html_attr } from '../../../utils/escape.js';
    +import { escape_html } from '../../../utils/escape.js';
     import { base64, sha256 } from './crypto.js';
     
     const array = new Uint8Array(16);
    @@ -300,7 +300,7 @@ class CspProvider extends BaseProvider {
     			return;
     		}
     
    -		return `<meta http-equiv="content-security-policy" content=${escape_html_attr(content)}>`;
    +		return `<meta http-equiv="content-security-policy" content="${escape_html(content, true)}">`;
     	}
     }
     
    
  • packages/kit/src/runtime/server/page/serialize_data.js+2 2 modified
    @@ -1,4 +1,4 @@
    -import { escape_html_attr } from '../../../utils/escape.js';
    +import { escape_html } from '../../../utils/escape.js';
     import { hash } from '../../hash.js';
     
     /**
    @@ -70,7 +70,7 @@ export function serialize_data(fetched, filter, prerendering = false) {
     	const attrs = [
     		'type="application/json"',
     		'data-sveltekit-fetched',
    -		`data-url=${escape_html_attr(fetched.url)}`
    +		`data-url="${escape_html(fetched.url, true)}"`
     	];
     
     	if (fetched.is_b64) {
    
  • packages/kit/src/utils/escape.js+36 20 modified
    @@ -6,41 +6,57 @@
     const escape_html_attr_dict = {
     	'&': '&amp;',
     	'"': '&quot;'
    +	// Svelte also escapes < because the escape function could be called inside a `noscript` there
    +	// https://github.com/sveltejs/svelte/security/advisories/GHSA-8266-84wp-wv5c
    +	// However, that doesn't apply in SvelteKit
     };
     
    +/**
    + * @type {Record<string, string>}
    + */
    +const escape_html_dict = {
    +	'&': '&amp;',
    +	'<': '&lt;'
    +};
    +
    +const surrogates = // high surrogate without paired low surrogate
    +	'[\\ud800-\\udbff](?![\\udc00-\\udfff])|' +
    +	// a valid surrogate pair, the only match with 2 code units
    +	// we match it so that we can match unpaired low surrogates in the same pass
    +	// TODO: use lookbehind assertions once they are widely supported: (?<![\ud800-udbff])[\udc00-\udfff]
    +	'[\\ud800-\\udbff][\\udc00-\\udfff]|' +
    +	// unpaired low surrogate (see previous match)
    +	'[\\udc00-\\udfff]';
    +
     const escape_html_attr_regex = new RegExp(
    -	// special characters
    -	`[${Object.keys(escape_html_attr_dict).join('')}]|` +
    -		// high surrogate without paired low surrogate
    -		'[\\ud800-\\udbff](?![\\udc00-\\udfff])|' +
    -		// a valid surrogate pair, the only match with 2 code units
    -		// we match it so that we can match unpaired low surrogates in the same pass
    -		// TODO: use lookbehind assertions once they are widely supported: (?<![\ud800-udbff])[\udc00-\udfff]
    -		'[\\ud800-\\udbff][\\udc00-\\udfff]|' +
    -		// unpaired low surrogate (see previous match)
    -		'[\\udc00-\\udfff]',
    +	`[${Object.keys(escape_html_attr_dict).join('')}]|` + surrogates,
    +	'g'
    +);
    +
    +const escape_html_regex = new RegExp(
    +	`[${Object.keys(escape_html_dict).join('')}]|` + surrogates,
     	'g'
     );
     
     /**
    - * Formats a string to be used as an attribute's value in raw HTML.
    - *
    - * It escapes unpaired surrogates (which are allowed in js strings but invalid in HTML), escapes
    - * characters that are special in attributes, and surrounds the whole string in double-quotes.
    + * Escapes unpaired surrogates (which are allowed in js strings but invalid in HTML) and
    + * escapes characters that are special.
      *
      * @param {string} str
    - * @returns {string} Escaped string surrounded by double-quotes.
    - * @example const html = `<tag data-value=${escape_html_attr('value')}>...</tag>`;
    + * @param {boolean} [is_attr]
    + * @returns {string} escaped string
    + * @example const html = `<tag data-value="${escape_html('value', true)}">...</tag>`;
      */
    -export function escape_html_attr(str) {
    -	const escaped_str = str.replace(escape_html_attr_regex, (match) => {
    +export function escape_html(str, is_attr) {
    +	const dict = is_attr ? escape_html_attr_dict : escape_html_dict;
    +	const escaped_str = str.replace(is_attr ? escape_html_attr_regex : escape_html_regex, (match) => {
     		if (match.length === 2) {
     			// valid surrogate pair
     			return match;
     		}
     
    -		return escape_html_attr_dict[match] ?? `&#${match.charCodeAt(0)};`;
    +		return dict[match] ?? `&#${match.charCodeAt(0)};`;
     	});
     
    -	return `"${escaped_str}"`;
    +	return escaped_str;
     }
    
  • packages/kit/src/utils/escape.spec.js+10 10 modified
    @@ -1,19 +1,19 @@
     import { assert, test } from 'vitest';
    -import { escape_html_attr } from './escape.js';
    +import { escape_html } from './escape.js';
     
     test('escape_html_attr escapes special attribute characters', () => {
     	assert.equal(
    -		escape_html_attr('some "values" are &special here, <others> aren\'t.'),
    -		'"some &quot;values&quot; are &amp;special here, <others> aren\'t."'
    +		escape_html('some "values" are &special here, <others> aren\'t.', true),
    +		"some &quot;values&quot; are &amp;special here, <others> aren't."
     	);
     });
     
     test('escape_html_attr escapes invalid surrogates', () => {
    -	assert.equal(escape_html_attr('\ud800\udc00'), '"\ud800\udc00"');
    -	assert.equal(escape_html_attr('\ud800'), '"&#55296;"');
    -	assert.equal(escape_html_attr('\udc00'), '"&#56320;"');
    -	assert.equal(escape_html_attr('\udc00\ud800'), '"&#56320;&#55296;"');
    -	assert.equal(escape_html_attr('\ud800\ud800\udc00'), '"&#55296;\ud800\udc00"');
    -	assert.equal(escape_html_attr('\ud800\udc00\udc00'), '"\ud800\udc00&#56320;"');
    -	assert.equal(escape_html_attr('\ud800\ud800\udc00\udc00'), '"&#55296;\ud800\udc00&#56320;"');
    +	assert.equal(escape_html('\ud800\udc00', true), '\ud800\udc00');
    +	assert.equal(escape_html('\ud800', true), '&#55296;');
    +	assert.equal(escape_html('\udc00', true), '&#56320;');
    +	assert.equal(escape_html('\udc00\ud800', true), '&#56320;&#55296;');
    +	assert.equal(escape_html('\ud800\ud800\udc00', true), '&#55296;\ud800\udc00');
    +	assert.equal(escape_html('\ud800\udc00\udc00', true), '\ud800\udc00&#56320;');
    +	assert.equal(escape_html('\ud800\ud800\udc00\udc00', true), '&#55296;\ud800\udc00&#56320;');
     });
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.