T09 · Insecure Skill Coding Practices
- Location
webhooks.md:36- Finding
Webhook Verification Does Not Enforce Timestamp Freshness
- Content
View full analysis
Vulnerability Details
File Location:
webhooks.md, lines 36–48
Vulnerability Type: Webhook replay vulnerability
Risk Level: MediumVulnerable Code
javascript function verifyWebhook(rawBody, signature, secret) { const ts = signature.split(';').find(p => p.startsWith('ts=')).split('=')[1]; const h1 = signature.split(';').find(p => p.startsWith('h1=')).split('=')[1]; const signedPayload = `${ts}:${rawBody}`; const expectedSig = crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(h1), Buffer.from(expectedSig) ); }The corresponding Python example has the same omission:
python def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool: parts = dict(p.split('=') for p in signature.split(';')) ts = parts['ts'] h1 = parts['h1'] signed_payload = f"{ts}:{raw_body.decode()}" expected_sig = hmac.new( secret.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(h1, expected_sig)Technical Analysis
Both webhook verification examples include the sender-provided timestamp in the HMAC input but never verify that the timestamp is recent. HMAC verification proves that a request was signed with the webhook secret, but without a freshness check it does not prove that the request is current.
An attacker who captures a legitimate webhook body and its
Paddle-Signatureheader can retransmit the unchanged request later. Because the timestamp remains part of the original signed payload, the HMAC remains valid indefinitely.The separate event-ID idempotency example can mitigate exact duplicate processing when it is implemented with durable, atomic storage. It does not replace timestamp validation and may not protect deployments that omit the option ...[truncated 1550 chars]
- Remediation
View remediation
Remediation Suggestions
- Parse
tsas a strict integer and reject invalid, missing, nonnumeric, or duplicated timestamp fields. - Compare the timestamp with the server's current time and reject requests outside a short configurable tolerance, such as five minutes.
- Perform freshness validation before processing or parsing the event body.
- Retain constant-time HMAC comparison after validating signature format and length.
- Continue using
event_idfor durable idempotency as a separate defense. - Make event processing and insertion of the processed-event record atomic, preferably in one database transaction with a unique constraint on
event_id. - Retain processed event IDs for a period aligned with Paddle's delivery and retry behavior.
- Ensure the server clock is synchronized through a trusted time source.
- Log rejection metadata without recording webhook secrets, complete signatures, or sensitive payload contents.
- Parse
