Fix Insecure Webhooks in Blitz.js
Webhooks in Blitz.js are just API routes, and if you aren't verifying the source, you're essentially providing an unauthenticated RCE or logic bypass vector. Attackers can spoof payloads to escalate privileges, manipulate billing, or trigger internal workflows. Relying on 'secret' URL paths is security through obscurity. You must implement cryptographic signature verification using a shared secret and timing-safe comparisons.
The Vulnerable Pattern
export default async function handler(req, res) { // VULNERABLE: No signature verification. // Anyone can POST any JSON to this endpoint. const { userId, eventType } = req.body;if (eventType === “subscription.deleted”) { await db.user.update({ where: { id: userId }, data: { plan: “free” }, }); }
res.status(200).json({ success: true }); }
The Secure Implementation
The secure implementation fixes three critical flaws: 1. It disables the default Blitz/Next.js bodyParser because verifying an HMAC requires the exact, byte-for-byte raw request body. 2. It calculates a SHA-256 HMAC using a server-side environment variable. 3. It uses crypto.timingSafeEqual to compare the provided signature against the calculated digest. This prevents timing attacks where an adversary guesses the signature byte-by-byte based on server response latency.
import { buffer } from "micro"; import crypto from "crypto";// Disable body parser to get raw body for signature check export const config = { api: { bodyParser: false, }, };
export default async function handler(req, res) { const signature = req.headers[“x-webhook-signature”]; const secret = process.env.WEBHOOK_SECRET;
if (!signature) return res.status(401).send(“Missing signature”);
const rawBody = await buffer(req); const hmac = crypto.createHmac(“sha256”, secret); const digest = hmac.update(rawBody).digest(“hex”);
// Use timingSafeEqual to prevent side-channel attacks const signatureBuffer = Buffer.from(signature); const digestBuffer = Buffer.from(digest);
if (signatureBuffer.length !== digestBuffer.length || !crypto.timingSafeEqual(signatureBuffer, digestBuffer)) { return res.status(401).send(“Invalid signature”); }
const event = JSON.parse(rawBody.toString()); // Proceed with validated logic… res.status(200).send(“Verified”); }
Your Blitz.js API
might be exposed to Insecure Webhooks
74% of Blitz.js apps fail this check. Hackers use automated scanners to find this specific flaw. Check your codebase before they do.
Free Tier • No Credit Card • Instant Report
Verified by Ghost Labs Security Team
This content is continuously validated by our automated security engine and reviewed by our research team. Ghost Labs analyzes over 500+ vulnerability patterns across 40+ frameworks to provide up-to-date remediation strategies.