From 6a38ba4adc4a162cc1f9c98931e1311b82911a10 Mon Sep 17 00:00:00 2001 From: openclawd Date: Sat, 14 Feb 2026 16:19:48 +0000 Subject: [PATCH] fix: critical security issues - webhook bypass, SSRF, XSS --- src/routes/billing.ts | 27 ++++++++++++++++----------- src/routes/convert.ts | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/routes/billing.ts b/src/routes/billing.ts index 16f8031..d1dfef1 100644 --- a/src/routes/billing.ts +++ b/src/routes/billing.ts @@ -2,6 +2,10 @@ import { Router, Request, Response } from "express"; import Stripe from "stripe"; import { createProKey, revokeByCustomer } from "../services/keys.js"; +function escapeHtml(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); +} + let _stripe: Stripe | null = null; function getStripe(): Stripe { if (!_stripe) { @@ -69,7 +73,7 @@ a { color: #4f9; }

🎉 Welcome to Pro!

Your API key:

-
${keyInfo.key}
+
${escapeHtml(keyInfo.key)}

Save this key! It won't be shown again.

10,000 PDFs/month • All endpoints • Priority support

View API docs →

@@ -87,16 +91,17 @@ router.post("/webhook", async (req: Request, res: Response) => { let event: Stripe.Event; - if (webhookSecret && sig) { - try { - event = getStripe().webhooks.constructEvent(req.body, sig, webhookSecret); - } catch (err: any) { - console.error("Webhook signature verification failed:", err.message); - res.status(400).json({ error: "Invalid signature" }); - return; - } - } else { - event = req.body as Stripe.Event; + if (!webhookSecret || !sig) { + res.status(400).json({ error: "Missing webhook secret or signature" }); + return; + } + + try { + event = getStripe().webhooks.constructEvent(req.body, sig, webhookSecret); + } catch (err: any) { + console.error("Webhook signature verification failed:", err.message); + res.status(400).json({ error: "Invalid signature" }); + return; } switch (event.type) { diff --git a/src/routes/convert.ts b/src/routes/convert.ts index 0ec69e1..796c40c 100644 --- a/src/routes/convert.ts +++ b/src/routes/convert.ts @@ -1,6 +1,24 @@ import { Router, Request, Response } from "express"; import { renderPdf, renderUrlPdf } from "../services/browser.js"; import { markdownToHtml, wrapHtml } from "../services/markdown.js"; +import dns from "node:dns/promises"; +import net from "node:net"; + +function isPrivateIP(ip: string): boolean { + // IPv6 loopback/unspecified + if (ip === "::1" || ip === "::") 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 + if (parts[0] === 127) return true; // 127.0.0.0/8 + if (parts[0] === 169 && parts[1] === 254) return true; // 169.254.0.0/16 + if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; // 172.16.0.0/12 + if (parts[0] === 192 && parts[1] === 168) return true; // 192.168.0.0/16 + return false; +} export const convertRouter = Router(); @@ -93,9 +111,10 @@ convertRouter.post("/url", async (req: Request, res: Response) => { return; } - // Basic URL validation + // URL validation + SSRF protection + let parsed: URL; try { - const parsed = new URL(body.url); + parsed = new URL(body.url); if (!["http:", "https:"].includes(parsed.protocol)) { res.status(400).json({ error: "Only http/https URLs are supported" }); return; @@ -105,6 +124,18 @@ convertRouter.post("/url", async (req: Request, res: Response) => { return; } + // DNS lookup to block private/reserved IPs + try { + const { address } = await dns.lookup(parsed.hostname); + if (isPrivateIP(address)) { + res.status(400).json({ error: "URL resolves to private/reserved IP" }); + return; + } + } catch { + res.status(400).json({ error: "DNS lookup failed for URL hostname" }); + return; + } + const pdf = await renderUrlPdf(body.url, { format: body.format, landscape: body.landscape,