T09 · Insecure Skill Coding Practices
- Location
SKILL.md:783- Finding
Insecure Webhook HMAC Signature Verification
- Content
View full analysis
Vulnerability Details
File Location:
SKILL.md, lines 783–799
Vulnerability Type: Insecure webhook authentication implementation
Risk Level: Mediumjavascript // HMAC signature verification (Stripe, GitHub, etc.) const crypto = require('crypto'); const signature = $request.headers['x-hub-signature-256']; const secret = $env.WEBHOOK_SECRET; const body = JSON.stringify($json); const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(body) .digest('hex'); if (signature !== expected) { // Return 401 via Respond to Webhook node return [{ json: { error: 'Invalid signature', _reject: true } }]; } return items;Technical Analysis
The example calculates the HMAC over
JSON.stringify($json), which serializes an already-parsed request object rather than authenticating the exact raw request bytes sent by the webhook provider. JSON parsing and reserialization can alter whitespace, property order, escaping, duplicate-key handling, Unicode representation, or number formatting. Because HMAC verification is byte-sensitive, these changes can cause the locally calculated digest to differ from the provider's valid signature.The code also compares attacker-controlled signature text using ordinary string inequality:
javascript signature !== expectedThis comparison is not guaranteed to be constant-time. Depending on the JavaScript runtime, surrounding infrastructure, network conditions, and number of available measurements, comparison timing can potentially reveal information about matching signature prefixes. Exploitability over a remote network may be limited by timing noise, but the pattern should not be presented as production-grade authentication.
The example further describes one implementation as applicable to “Stripe, GitHub, etc.” while using GitHub's
x-hub-signature-256format. Webhook providers use different headers, signed payload formats, time ...[truncated 2042 chars]- Remediation
View remediation
Remediation Suggestions
- Capture and verify the exact raw request bytes before JSON parsing or transformation. Configure n8n or the upstream proxy to preserve the raw body in a byte buffer.
- Use a provider-specific verification implementation or the provider's official SDK. Do not reuse GitHub's header format for Stripe or other services.
- Parse and validate the signature header strictly, including the expected algorithm, encoding, and number of signature values.
- Convert the supplied and calculated digests into buffers, confirm that their lengths are equal, and compare them with
crypto.timingSafeEqual. - Where the provider includes a signed timestamp, enforce a narrow acceptance window and reject stale events to prevent replay.
- Store processed provider event IDs and reject duplicates when the provider supplies stable event identifiers.
- Return an explicit authentication failure without forwarding rejected items to downstream nodes.
- Add tests using official provider fixtures, including raw bodies with alternate whitespace, Unicode, escaped characters, and reordered properties, to confirm that valid events pass and modified events fail.
- Update the Skill documentation to explain that raw-body access and verification requirements differ among webhook providers.
