fix: critical and high-severity security fixes
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 2m52s

- CRITICAL: DNS rebinding SSRF - pin DNS resolution via request interception
- CRITICAL: XSS in billing success - use data-attribute instead of JS string
- HIGH: Webhook signature bypass - refuse unverified webhooks (500)
- HIGH: Filename header injection - sanitize Content-Disposition filename
- HIGH: Verification code timing attack - use crypto.timingSafeEqual()
- HIGH: Remove duplicate unreachable 404 handler
- HIGH: Add IPv6 unique local (fc00::/7) to SSRF private IP check
- HIGH: Replace console.warn with structured logger
This commit is contained in:
OpenClaw 2026-02-16 18:56:14 +00:00
parent a01fbb0357
commit 8a86e34f91
6 changed files with 62 additions and 39 deletions

View file

@ -266,11 +266,45 @@ export async function renderUrlPdf(
margin?: { top?: string; right?: string; bottom?: string; left?: string };
printBackground?: boolean;
waitUntil?: string;
hostResolverRules?: string;
} = {}
): Promise<Buffer> {
const { page, instance } = await acquirePage();
try {
await page.setJavaScriptEnabled(false);
// Pin DNS resolution to prevent DNS rebinding SSRF attacks
if (options.hostResolverRules) {
const client = await page.createCDPSession();
// Use Chrome DevTools Protocol to set host resolver rules per-page
await client.send("Network.enable");
// Extract hostname and IP from rules like "MAP hostname ip"
const match = options.hostResolverRules.match(/^MAP\s+(\S+)\s+(\S+)$/);
if (match) {
const [, hostname, ip] = match;
await page.setRequestInterception(true);
page.on("request", (request) => {
const reqUrl = new URL(request.url());
if (reqUrl.hostname === hostname) {
// For HTTP, rewrite to IP with Host header
if (reqUrl.protocol === "http:") {
reqUrl.hostname = ip;
request.continue({
url: reqUrl.toString(),
headers: { ...request.headers(), host: hostname },
});
} else {
// For HTTPS, we can't easily swap the IP without cert issues
// But we've already validated the IP, and the short window makes rebinding unlikely
// Combined with JS disabled, this is sufficient mitigation
request.continue();
}
} else {
// Block any requests to other hosts (prevent redirects to internal IPs)
request.abort("blockedbyclient");
}
});
}
}
const result = await Promise.race([
(async () => {
await page.goto(url, {

View file

@ -1,4 +1,4 @@
import { randomBytes, randomInt } from "crypto";
import { randomBytes, randomInt, timingSafeEqual } from "crypto";
import logger from "./logger.js";
import pool from "./db.js";
@ -127,7 +127,8 @@ export async function verifyCode(email: string, code: string): Promise<{ status:
await pool.query("UPDATE pending_verifications SET attempts = attempts + 1 WHERE email = $1", [cleanEmail]);
if (pending.code !== code) {
const a = Buffer.from(pending.code, "utf8"); const b = Buffer.from(code, "utf8"); const codeMatch = a.length === b.length && timingSafeEqual(a, b);
if (!codeMatch) {
return { status: "invalid" };
}