Fix SSRF vulnerability: Add IPv6 link-local blocking and update error message
Some checks failed
Deploy to Production / Deploy to Server (push) Failing after 20s

- Add fe80::/10 (IPv6 link-local) detection to isPrivateIP()
- Update error message to match specification: 'URL resolves to a private/internal IP address'
- Existing protections already covered all required IPv4 ranges and IPv6 localhost
This commit is contained in:
openclawd 2026-02-16 08:36:08 +00:00
parent 86f8da62ec
commit 7b55a1ddc6

View file

@ -8,9 +8,15 @@ import net from "node:net";
function isPrivateIP(ip: string): boolean { function isPrivateIP(ip: string): boolean {
// IPv6 loopback/unspecified // IPv6 loopback/unspecified
if (ip === "::1" || ip === "::") return true; if (ip === "::1" || ip === "::") return true;
// IPv6 link-local (fe80::/10)
if (ip.toLowerCase().startsWith("fe8") || ip.toLowerCase().startsWith("fe9") ||
ip.toLowerCase().startsWith("fea") || ip.toLowerCase().startsWith("feb")) return true;
// IPv4-mapped IPv6 // IPv4-mapped IPv6
if (ip.startsWith("::ffff:")) ip = ip.slice(7); if (ip.startsWith("::ffff:")) ip = ip.slice(7);
if (!net.isIPv4(ip)) return false; if (!net.isIPv4(ip)) return false;
const parts = ip.split(".").map(Number); const parts = ip.split(".").map(Number);
if (parts[0] === 0) return true; // 0.0.0.0/8 if (parts[0] === 0) return true; // 0.0.0.0/8
if (parts[0] === 10) return true; // 10.0.0.0/8 if (parts[0] === 10) return true; // 10.0.0.0/8
@ -160,7 +166,7 @@ convertRouter.post("/url", async (req: Request & { acquirePdfSlot?: () => Promis
try { try {
const { address } = await dns.lookup(parsed.hostname); const { address } = await dns.lookup(parsed.hostname);
if (isPrivateIP(address)) { if (isPrivateIP(address)) {
res.status(400).json({ error: "URL resolves to private/reserved IP" }); res.status(400).json({ error: "URL resolves to a private/internal IP address" });
return; return;
} }
} catch { } catch {