Problem
safeFetch enforces maxSize in two places, but both checks happen after the full response body has already been materialized in memory:
- The
content-length header check is advisory — a malicious server can omit or lie about it.
await res.arrayBuffer() buffers the entire response body into a single ArrayBuffer before the byte-length check runs. By the time we check ab.byteLength > maxSize, the memory has already been allocated.
A malicious image URL pointing to a server that omits content-length and streams an endlessly large body causes the main process to allocate gigabytes of memory before the check fires, leading to an OOM crash of the entire Electron app.
Location
File: packages/desktop/src/main/net/safe-fetch.ts:21-27
const res = await net.fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`fetch ${url}: ${res.status}`);
const cl = Number(res.headers.get('content-length') ?? '0');
if (cl > maxSize) throw new Error('response too large');
const ab = await res.arrayBuffer(); // ← entire body buffered here
if (ab.byteLength > maxSize) throw new Error('response too large');
return Buffer.from(ab);
Fix Approach
Switch to a streaming reader that enforces maxSize incrementally and aborts as soon as the limit is exceeded:
const reader = res.body?.getReader();
if (!reader) throw new Error('no body');
const chunks: Uint8Array[] = [];
let total = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > maxSize) {
controller.abort();
throw new Error('response too large');
}
chunks.push(value);
}
return Buffer.concat(chunks.map((c) => Buffer.from(c)));
Keep the initial content-length pre-check as a cheap short-circuit for well-behaved servers.
Verification
- Run
pnpm check — must pass.
- Unit test: mock
net.fetch to return a body that streams more than maxSize bytes with no content-length header; assert safeFetch throws before allocating more than maxSize bytes.
- Manual: point
safeFetch at a test server that streams /dev/urandom; confirm memory usage stays bounded.
Context
- WG: Observability & DX (security — memory DoS)
- Priority: Medium
- Estimated effort: 1 hour
Problem
safeFetchenforcesmaxSizein two places, but both checks happen after the full response body has already been materialized in memory:content-lengthheader check is advisory — a malicious server can omit or lie about it.await res.arrayBuffer()buffers the entire response body into a singleArrayBufferbefore the byte-length check runs. By the time we checkab.byteLength > maxSize, the memory has already been allocated.A malicious image URL pointing to a server that omits
content-lengthand streams an endlessly large body causes the main process to allocate gigabytes of memory before the check fires, leading to an OOM crash of the entire Electron app.Location
File:
packages/desktop/src/main/net/safe-fetch.ts:21-27Fix Approach
Switch to a streaming reader that enforces
maxSizeincrementally and aborts as soon as the limit is exceeded:Keep the initial
content-lengthpre-check as a cheap short-circuit for well-behaved servers.Verification
pnpm check— must pass.net.fetchto return a body that streams more thanmaxSizebytes with nocontent-lengthheader; assertsafeFetchthrows before allocating more thanmaxSizebytes.safeFetchat a test server that streams/dev/urandom; confirm memory usage stays bounded.Context