Problem
isPrivateIPv6 only detects loopback through two literal string comparisons: lower === '::' and lower === '::1'. IPv6 allows multiple equivalent representations of the same address, and two common forms slip through the current check:
- Uncompressed:
0000:0000:0000:0000:0000:0000:0000:0001 — valid per RFC 5952, recognized by node:net's isIP as v6, but the literal string comparison fails. The subsequent first & 0xfe00 / first & 0xffc0 masks produce 0, which does not match the ULA (fc00::/7) or link-local (fe80::/10) ranges. Result: returns false → guard bypassed.
- Alternate compression:
0::1 — equivalent to ::1, valid, also slips through the literal comparison and fails the mask checks.
Both forms can reach the renderer-triggered safeFetch path via the image URL, meaning a crafted URL like https://[0000:0000:0000:0000:0000:0000:0000:0001]/... bypasses SSRF protection.
Location
File: packages/desktop/src/main/net/ssrf-guard.ts:21-36
function isPrivateIPv6(ip: string): boolean {
const lower = ip.toLowerCase();
if (lower === '::' || lower === '::1') return true;
const first = parseInt(lower.split(':')[0], 16);
if ((first & 0xfe00) === 0xfc00) return true;
if ((first & 0xffc0) === 0xfe80) return true;
// ...
return false;
}
Fix Approach
Normalize the address to canonical form before comparison. Two options:
- Simple: expand the input yourself — split on
:, handle the :: zero-run, pad each hextet to 4 hex digits, join. Then compare against 0000:0000:0000:0000:0000:0000:0000:0000 and 0000:0000:0000:0000:0000:0000:0000:0001.
- Pragmatic: use
Buffer.from(...) with the ipaddr.js library (already transitively available via many packages) to parse into a 16-byte array, then check all-zero or all-zero-except-last-byte-equals-1.
After normalization, extend the check to also catch any address whose upper 112 bits are zero and lower 16 bits are 0 or 1.
Verification
- Run
pnpm check — must pass.
- Unit tests for each of these inputs — all must return
true:
::
::1
0:0:0:0:0:0:0:1
0000:0000:0000:0000:0000:0000:0000:0001
0::1
0000::1
Context
- WG: Observability & DX (security)
- Priority: Low (good first issue — contained, test-driven fix)
- Estimated effort: 30-45 minutes
Problem
isPrivateIPv6only detects loopback through two literal string comparisons:lower === '::'andlower === '::1'. IPv6 allows multiple equivalent representations of the same address, and two common forms slip through the current check:0000:0000:0000:0000:0000:0000:0000:0001— valid per RFC 5952, recognized bynode:net'sisIPas v6, but the literal string comparison fails. The subsequentfirst & 0xfe00/first & 0xffc0masks produce0, which does not match the ULA (fc00::/7) or link-local (fe80::/10) ranges. Result: returnsfalse→ guard bypassed.0::1— equivalent to::1, valid, also slips through the literal comparison and fails the mask checks.Both forms can reach the renderer-triggered
safeFetchpath via the image URL, meaning a crafted URL likehttps://[0000:0000:0000:0000:0000:0000:0000:0001]/...bypasses SSRF protection.Location
File:
packages/desktop/src/main/net/ssrf-guard.ts:21-36Fix Approach
Normalize the address to canonical form before comparison. Two options:
:, handle the::zero-run, pad each hextet to 4 hex digits, join. Then compare against0000:0000:0000:0000:0000:0000:0000:0000and0000:0000:0000:0000:0000:0000:0000:0001.Buffer.from(...)with theipaddr.jslibrary (already transitively available via many packages) to parse into a 16-byte array, then check all-zero or all-zero-except-last-byte-equals-1.After normalization, extend the check to also catch any address whose upper 112 bits are zero and lower 16 bits are
0or1.Verification
pnpm check— must pass.true:::::10:0:0:0:0:0:0:10000:0000:0000:0000:0000:0000:0000:00010::10000::1Context