SvelteKit Denial of service and possible SSRF when using prerendering
Description
SvelteKit is a framework for rapidly developing robust, performant web applications using Svelte. Prior to 2.49.5, SvelteKit is vulnerable to a server side request forgery (SSRF) and denial of service (DoS) under certain conditions. From 2.44.0 through 2.49.4, the vulnerability results in a DoS when your app has at least one prerendered route (export const prerender = true). From 2.19.0 through 2.49.4, the vulnerability results in a DoS when your app has at least one prerendered route and you are using adapter-node without a configured ORIGIN environment variable, and you are not using a reverse proxy that implements Host header validation. This vulnerability is fixed in 2.49.5.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@sveltejs/kitnpm | >= 2.19.0, < 2.49.5 | 2.49.5 |
@sveltejs/adapter-nodenpm | >= 5.4.1, < 5.5.1 | 5.5.1 |
Affected products
1Patches
14 files changed · +73 −18
.changeset/six-geese-ring.md+5 −0 added@@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +fix: add validations for protocol, host, and port header values
.changeset/slow-pears-follow.md+5 −0 added@@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: ensure url decoded pathnames are not mistaken as rerouted requests
packages/adapter-node/src/handler.js+43 −4 modified@@ -179,14 +179,53 @@ function sequence(handlers) { }; } +/** + * @param {string} name + * @param {string | string[] | undefined} value + * @returns {string | undefined} + */ +function normalise_header(name, value) { + if (!name) return undefined; + if (Array.isArray(value)) { + if (value.length === 0) return undefined; + if (value.length === 1) return value[0]; + throw new Error( + `Multiple values provided for ${name} header where only one expected: ${value}` + ); + } + return value; +} + /** * @param {import('http').IncomingHttpHeaders} headers - * @returns + * @returns {string} */ function get_origin(headers) { - const protocol = (protocol_header && headers[protocol_header]) || 'https'; - const host = (host_header && headers[host_header]) || headers['host']; - const port = port_header && headers[port_header]; + const protocol = decodeURIComponent(normalise_header(protocol_header, headers[protocol_header]) || 'https'); + + // this helps us avoid host injections through the protocol header + if (protocol.includes(':')) { + throw new Error( + `The ${protocol_header} header specified ${protocol} which is an invalid because it includes \`:\`. It should only contain the protocol scheme (e.g. \`https\`)` + ); + } + + const host = + normalise_header(host_header, headers[host_header]) || + normalise_header('host', headers['host']); + if (!host) { + const header_names = host_header ? `${host_header} or host headers` : 'host header'; + throw new Error( + `Could not determine host. The request must have a value provided by the ${header_names}` + ); + } + + const port = normalise_header(port_header, headers[port_header]); + if (port && isNaN(+port)) { + throw new Error( + `The ${port_header} header specified ${port} which is an invalid port because it is not a number. The value should only contain the port number (e.g. 443)` + ); + } return port ? `${protocol}://${host}:${port}` : `${protocol}://${host}`; }
packages/kit/src/runtime/server/respond.js+20 −14 modified@@ -247,8 +247,10 @@ export async function internal_respond(request, options, manifest, state) { return text('Malformed URI', { status: 400 }); } + // try to serve the rerouted prerendered resource if it exists if ( - resolved_path !== url.pathname && + // the resolved path has been decoded so it should be compared to the decoded url pathname + resolved_path !== decode_pathname(url.pathname) && !state.prerendering?.fallback && has_prerendered_path(manifest, resolved_path) ) { @@ -259,20 +261,24 @@ export async function internal_respond(request, options, manifest, state) { ? add_resolution_suffix(resolved_path) : resolved_path; - // `fetch` automatically decodes the body, so we need to delete the related headers to not break the response - // Also see https://github.com/sveltejs/kit/issues/12197 for more info (we should fix this more generally at some point) - const response = await fetch(url, request); - const headers = new Headers(response.headers); - if (headers.has('content-encoding')) { - headers.delete('content-encoding'); - headers.delete('content-length'); - } + try { + // `fetch` automatically decodes the body, so we need to delete the related headers to not break the response + // Also see https://github.com/sveltejs/kit/issues/12197 for more info (we should fix this more generally at some point) + const response = await fetch(url, request); + const headers = new Headers(response.headers); + if (headers.has('content-encoding')) { + headers.delete('content-encoding'); + headers.delete('content-length'); + } - return new Response(response.body, { - headers, - status: response.status, - statusText: response.statusText - }); + return new Response(response.body, { + headers, + status: response.status, + statusText: response.statusText + }); + } catch (error) { + return await handle_fatal_error(event, event_state, options, error); + } } /** @type {import('types').SSRRoute | null} */
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- github.com/advisories/GHSA-j62c-4x62-9r35ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-67647ghsaADVISORY
- github.com/sveltejs/kit/commit/d9ae9b00b14f5574d109f3fd548f960594346226ghsax_refsource_MISCWEB
- github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fadapter-node%405.5.1ghsaWEB
- github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%402.49.5ghsaWEB
- github.com/sveltejs/kit/security/advisories/GHSA-j62c-4x62-9r35ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.