T09 · Insecure Skill Coding Practices
- Location
references/examples.md:218- Finding
Webhook signature verification uses an unavailable raw request body
- Content
View full analysis
{ // Verify signature (see Signature Verification section) const isValid = verifySignature(req); ``` `references/webhooks.md:240-258`: ```javascript const crypto = require('crypto'); function verifySignature(req) { const signature = req.get('X-Miro-Request-Signature'); const secret = process.env.MIRO_WEBHOOK_SECRET; const body = req.rawBody; // Store raw body as string const hmac = crypto .createHmac('sha256', secret) .update(body) .digest('hex'); return signature === `sha256=${hmac}`; } // Middleware to capture raw body app.use(express.raw({type: 'application/json'})); ``` ### Technical Analysis An HMAC webhook signature must be calculated over the exact request bytes received from the sender. `express.json()` parses and consumes the request body but does not create `req.rawBody`. Consequently, the example in `references/examples.md` passes `undefined` to `Hmac.update()`. The webhook guide has a related middleware-ordering problem: `express.raw()` is shown aft ...[truncated 2282 chars]- Remediation
View remediation
{ const supplied = req.get('X-Miro-Request-Signature'); const body = req.body; if (!supplied || !Buffer.isBuffer(body)) { return res.status(401).send('Unauthorized'); } const expected = `sha256=${crypto .createHmac('sha256', webhookSecret) .update(body) .digest('hex')}`; const suppliedBuffer = Buffer.from(supplied, 'utf8'); const expectedBuffer = Buffer.from(expected, 'utf8'); if ( suppliedBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(suppliedBuffer, expectedBuffer) ) { return res.status(401).send('Unauthorized'); } let event; try { event = JSON.parse(body.toString('utf8')); } catch { return res.status(400).send('Invalid JSON'); } res.status(200).send('OK'); setImmediate(() => handleEvent(event)); } ); ``` ]]>
