[Bug] safeFetch is vulnerable to DNS rebinding TOCTOU (SSRF bypass)
#405 aperta il 15 apr 2026
Metriche repository
- Star
- (519 star)
- Metriche merge PR
- (Metriche PR in attesa)
Descrizione
Problem
assertNotPrivateHost resolves the hostname via dns.resolve4/resolve6, checks the resulting IPs against a private-range blocklist, and returns. Immediately after, safeFetch calls net.fetch(url, ...) with the original URL string, which performs its own independent DNS resolution. A DNS rebinding attacker can return a public IP on the first lookup (passing the guard) and a private IP on the second lookup (the actual fetch), completely bypassing the SSRF check. This is directly triggerable from the renderer via artifact:save-image-url and the auto-extract path.
Location
File: packages/desktop/src/main/net/safe-fetch.ts:12-27 + packages/desktop/src/main/net/ssrf-guard.ts:49-59
// safe-fetch.ts
export async function safeFetch(url: string, opts: SafeFetchOptions = {}): Promise<Buffer> {
const parsed = new URL(url);
if (parsed.protocol !== 'https:') throw new Error('HTTPS required');
await assertNotPrivateHost(parsed.hostname); // ← first DNS resolution
// ...
const res = await net.fetch(url, { signal: controller.signal }); // ← second independent DNS resolution
Attack surface: both call sites (packages/desktop/src/main/artifact/auto-extract.ts:35 and packages/desktop/src/main/ipc/artifact-handlers.ts:161) pass renderer-controlled URLs directly into safeFetch.
Fix Approach
Pin the IP between the guard and the fetch so the two operations cannot disagree. Options:
- Preferred: do the DNS resolution yourself in
assertNotPrivateHost, return the resolved IP, and pass that IP tonet.fetchvia a rewritten URL (withHostheader preserved for TLS SNI).net.fetchin Electron supports aHostheader override. - Alternative: wrap
dns.lookupwith a short-lived cache keyed by hostname that both the guard and the fetch read through, so both observe the same resolution result.
Either approach closes the TOCTOU window. The current code cannot be fixed in-place without pinning.
Verification
- Run
pnpm check— must pass. - Unit test: mock
net.fetchto observe the resolved IP it is given; confirm it matches what the guard saw. - Manual: set up a rebinding test server that flips between
1.1.1.1and127.0.0.1across two DNS queries; confirmsafeFetchrejects.
Context
- WG: Observability & DX (also security-critical)
- Priority: Medium (security, but attack surface is limited to user-supplied image URLs)
- Estimated effort: 1-2 hours