T09 · Insecure Skill Coding Practices
- Location
references/api.md:172- Finding
Non-Constant-Time Webhook Signature Comparison in Node.js Example
- Content
View full analysis
Vulnerability Details
File Location:
references/api.md, lines 172–177
Vulnerability Type: Timing side channel in authentication verification
Risk Level: Lowjavascript const crypto = require("crypto"); const expected = crypto.createHmac("sha256", signingSecret) .update(rawBody).digest("hex"); if (expected !== req.headers["x-rizzforms-signature"]) { return res.status(401).end(); }Technical Analysis
The documented Node.js webhook-verification example compares the calculated HMAC and the supplied signature using JavaScript's ordinary string inequality operator (
!==). Ordinary string comparisons are not guaranteed to run in constant time: execution may vary according to properties of the compared values, potentially including the position of the first differing character.Because this reference is intended to be copied into webhook handlers, it may lead users to implement signature authentication with a timing side channel. The Ruby and Python examples in the same document appropriately use constant-time comparison functions, but the Node.js example does not.
Exploitation requires an attacker to submit a large number of chosen signatures and obtain sufficiently precise timing measurements. Network jitter, runtime optimization, and the absence of a guaranteed character-by-character comparison make remote exploitation difficult and environment-dependent; therefore, the finding is rated Low rather than treated as a reliable signature bypass.
Attack Path
- An attacker identifies a webhook handler implemented from the documented Node.js example.
- The attacker sends repeated requests containing the same chosen body and controlled
X-RizzForms-Signaturevalues. - For each candidate signature prefix, the attacker gathers many response-time measurements to reduce network noise.
- If the runtime's comparison behavior creates a measurable prefix-dependent timing difference, the attack ...[truncated 1051 chars]
- Remediation
View remediation
Remediation Suggestions
Replace the ordinary string comparison with Node.js
crypto.timingSafeEqual()over equal-length byte buffers. Validate the signature format and length before comparison becausetimingSafeEqual()throws when buffer lengths differ.javascript const crypto = require("crypto"); const expected = crypto .createHmac("sha256", signingSecret) .update(rawBody) .digest(); const suppliedHex = req.headers["x-rizzforms-signature"]; if ( typeof suppliedHex !== "string" || !/^[0-9a-fA-F]{64}$/.test(suppliedHex) ) { return res.status(401).end(); } const supplied = Buffer.from(suppliedHex, "hex"); if ( supplied.length !== expected.length || !crypto.timingSafeEqual(expected, supplied) ) { return res.status(401).end(); }Additional hardening measures should include:
- Verify the signature against the exact raw request bytes before JSON parsing or body transformation.
- Reject missing, malformed, duplicated, or unexpectedly encoded signature headers.
- Apply request-size limits and rate limiting to reduce timing sampling opportunities.
- Avoid logging signing secrets or complete attacker-supplied authentication headers.
- Add tests covering valid signatures, invalid signatures, malformed hexadecimal input, missing headers, and unequal lengths.
- Document secret rotation and ensure old signing secrets are invalidated according to the intended rotation policy.
