Feb 19, 2026·6 min read·3 visits
If you enabled 'experimental.remoteFunctions' in SvelteKit < 2.52.2, your server is vulnerable to DoS. A malformed binary form payload can force the server into an expensive recursive type coercion loop.
An experimental feature in SvelteKit introduced a severe Denial of Service vulnerability via the binary form deserialization logic. By exploiting how the 'devalue' library handles object references and type coercion, an attacker can trigger massive CPU consumption.
Developers love experimental features. It's the software equivalent of an 'Uncharted Territory' sign—irresistible. SvelteKit introduced remoteFunctions to allow seamless RPC-like calls between the client and server. To make this efficient, they needed something better than JSON. Enter devalue, a library that handles complex types like Map, Set, and cyclic references that JSON.stringify would choke on.
But here's the kicker: to support file uploads within this remote function architecture, SvelteKit implemented a custom binary deserializer for application/x-sveltekit-formdata. This deserializer reads a stream of bytes and reconstructs the form data, including file metadata. It's clever, efficient, and, as it turns out, completely trusting of the input it receives.
The vulnerability lies in how this deserializer reconstructs File objects. It assumes the metadata provided by the client (name, size, type) is well-formed. Spoiler alert: in the world of security, assuming input is well-formed is like leaving your front door unlocked because you live in a 'nice neighborhood'.
The root cause here is a classic JavaScript foot-gun: Implicit Type Coercion. When the deserialize_binary_form function processes the incoming byte stream, it encounters a definition for a File. It expects an array containing metadata: [name, type, size, lastModified, index].
The code then attempts to verify that the file data doesn't exceed the content length of the request. It does this with a simple arithmetic check:
// The code blindly trusts 'size' is a number
if (files_start_offset + file_offsets[index] + size > content_length) {
throw deserialize_error('file data overflow');
}See the problem? In JavaScript, the + operator and > comparison are not strictly typed. If size is not a number, the engine will try to make it a number. If an attacker provides a complex object (like a deeply nested array or a proxy to a huge BigInt) instead of a simple integer, the JavaScript engine triggers valueOf() or toString().
Because devalue allows for defining complex, recursive object graphs, an attacker can craft a payload where size references a massive, self-referential structure. The engine burns 100% CPU trying to coerce this monstrosity into a primitive number just to evaluate that if statement.
Let's look at the diff. It's a perfect example of "we forgot to check types." The fix, introduced in version 2.52.2, essentially says: "I don't care what you think you are, you better be a number."
Here is the vulnerable logic versus the patched logic in packages/kit/src/runtime/form-utils.js:
Before (Vulnerable):
File: ([name, type, size, last_modified, index]) => {
// Yolo. Let's do math on whatever 'size' is.
if (files_start_offset + file_offsets[index] + size > content_length) {
throw deserialize_error('file data overflow');
}
return new File(/*...*/);
}After (Patched):
File: ([name, type, size, last_modified, index]) => {
// The bouncer checks ID at the door.
if (
typeof name !== 'string' ||
typeof type !== 'string' ||
typeof size !== 'number' ||
typeof last_modified !== 'number' ||
typeof index !== 'number'
) {
throw deserialize_error('invalid file metadata');
}
if (files_start_offset + file_offsets[index] + size > content_length) {
throw deserialize_error('file data overflow');
}
// ...
}The fix is boring, effectively just a block of typeof checks. But boring is safe. Boring is reliable.
To exploit this, we don't need a buffer overflow or a ROP chain. We just need to understand devalue serialization format. We construct a payload where the size property of a file definition points to a recursive array structure.
Here is a conceptual breakdown of the attack vector:
size) is a reference to that array.remoteFunctions.When the server parses this, it hits the size > content_length check. The engine tries to convert that recursive array to a number. It hangs.
// Conceptual PoC Payload Generator
const payload = JSON.stringify([
// Index 0: The trap. A recursive structure.
[...Array(100)].map(() => 0), // Base array
// ... complex cross-references ...
// The Trigger: A File object definition
// [name, type, size (pointing to recursive structure), lastMod, index]
['malicious.txt', 'text/plain', ['BigInt', 1e100], 123456789, 0]
]);The actual PoC found in the regression tests uses a mix of deep arrays and BigInt coercions to maximize the CPU spin. Since Node.js is single-threaded for the event loop, one such request effectively kills the server for everyone else.
This is a Denial of Service (DoS) vulnerability. In a microservices environment, losing one node might be annoying. But for a monolithic SvelteKit application, this is fatal.
Because the exhaustion happens synchronously during the request parsing phase (before your business logic even runs), you cannot catch this with standard application-level try/catch blocks easily. The event loop locks up. Health checks fail. Kubernetes kills the pod. The pod restarts. The attacker sends another request.
It's an asymmetric attack: The attacker spends almost zero resources sending a small JSON payload, while your server spends 100% CPU trying to figure out if [Array(10000)] > 100.
The remediation is straightforward. If you are using @sveltejs/kit versions prior to 2.52.2, you have two options:
2.52.2 immediately. This includes the patch that enforces strict type checking on deserialized file metadata.svelte.config.js. If you have experimental: { remoteFunctions: true }, set it to false or remove it. This feature is opt-in, so if you didn't explicitly turn it on, you are safe.Always treat experimental flags as exactly what they are: code that hasn't been battle-hardened yet.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
@sveltejs/kit Svelte | < 2.52.2 | 2.52.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-400 (Uncontrolled Resource Consumption) |
| Attack Vector | Network (Remote) |
| CVSS Score | 7.5 (High) |
| Impact | Denial of Service (CPU Exhaustion) |
| Exploit Status | PoC Available |
| Affected Component | remoteFunctions / devalue deserializer |