From 7b55a1ddc67fcf712e96076f7bd34a58036df449 Mon Sep 17 00:00:00 2001 From: openclawd Date: Mon, 16 Feb 2026 08:36:08 +0000 Subject: [PATCH] Fix SSRF vulnerability: Add IPv6 link-local blocking and update error message - 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 --- src/routes/convert.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/routes/convert.ts b/src/routes/convert.ts index 6266f24..4ee2292 100644 --- a/src/routes/convert.ts +++ b/src/routes/convert.ts @@ -8,9 +8,15 @@ import net from "node:net"; function isPrivateIP(ip: string): boolean { // IPv6 loopback/unspecified 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 if (ip.startsWith("::ffff:")) ip = ip.slice(7); if (!net.isIPv4(ip)) return false; + const parts = ip.split(".").map(Number); if (parts[0] === 0) return true; // 0.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 { const { address } = await dns.lookup(parsed.hostname); 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; } } catch {