Fix Insecure Webhooks in Meteor
Webhooks in Meteor are often implemented via 'WebApp.connectHandlers'. The common failure mode is 'blind trust': accepting incoming POST requests without cryptographic verification of the sender. An attacker can easily spoof payloads to trigger unauthorized actions, such as marking unpaid orders as 'paid' or escalating user privileges. To fix this, you must implement HMAC signature verification using a shared secret.
The Vulnerable Pattern
import { WebApp } from 'meteor/webapp'; import bodyParser from 'body-parser';
// VULNERABLE: No signature verification. Anyone can POST to this endpoint. WebApp.connectHandlers.use(‘/webhooks/payment’, bodyParser.json()); WebApp.connectHandlers.use(‘/webhooks/payment’, (req, res) => { const { orderId, status } = req.body; if (status === ‘completed’) { Orders.update(orderId, { $set: { paid: true } }); } res.writeHead(200); res.end(‘OK’); });
The Secure Implementation
The secure implementation ditches generic body-parser middleware to capture the 'raw' buffer. This is critical because re-stringifying a parsed object often changes the byte-order or whitespace, breaking the hash. We compute an HMAC-SHA256 hash of the raw body using a pre-shared secret and compare it to the provider's header using 'crypto.timingSafeEqual'. This ensures the request is authentic and hasn't been tampered with in transit. If the hashes don't match, we drop the request immediately.
import { WebApp } from 'meteor/webapp'; import crypto from 'crypto';const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
WebApp.connectHandlers.use(‘/webhooks/payment’, (req, res, next) => { let rawBody = ”; req.on(‘data’, (chunk) => { rawBody += chunk; }); req.on(‘end’, () => { const signature = req.headers[‘x-hub-signature-256’]; if (!signature) { res.writeHead(401); return res.end(‘Missing signature’); }
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET); const digest = 'sha256=' + hmac.update(rawBody).digest('hex'); // Use timingSafeEqual to prevent timing attacks const signatureBuffer = Buffer.from(signature); const digestBuffer = Buffer.from(digest); if (signatureBuffer.length !== digestBuffer.length || !crypto.timingSafeEqual(signatureBuffer, digestBuffer)) { res.writeHead(403); return res.end('Invalid signature'); } try { req.body = JSON.parse(rawBody); // Logic to update DB goes here... res.writeHead(200); res.end('Verified'); } catch (e) { res.writeHead(400); res.end('Invalid JSON'); }
}); });
Your Meteor API
might be exposed to Insecure Webhooks
74% of Meteor 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.