Review: Order Paid Webhook Written by an AI Assistant
Your team asked an AI coding assistant to mark orders as paid when the payment provider sends a webhook. It opened this pull request. You are the reviewer, and nothing merges without your approval.
The assistant's description:
AddsPOST /webhooks/payments. Every event is verified with an HMAC-SHA256 signature before anything is trusted. Oncharge.succeededthe order is marked as paid and the customer gets a receipt email.
Where it runs: a public Flask app. The provider signs the raw request body it sends (compact JSON) and puts the hex digest in X-Signature. It may deliver the same event more than once, and it retries any delivery that does not get a 2xx response, for up to three days. Each event has a unique id. The secret comes from an environment variable; staging and preview environments run the same code, and not all of them set it.
Your task
- Flag every problem you would block the merge for. Click a line number to comment, and Shift-click to cover a range.
- For each one, choose the issue type and severity, and explain what fails and how to fix it.
- You are scored on the problems you catch, the right issue type and a clear explanation. Missing a critical problem fails the round.
Aim for 10 minutes, as in a real review round.
What happens when PAYMENTS_WEBHOOK_SECRET is not set? Who else can compute that HMAC?
The provider signed the exact bytes it sent. Are those the bytes being checked?
How is the signature compared, and what can an attacker learn from how long the comparison takes?
The provider delivers the same charge.succeeded event twice. What does the customer receive?
What happens when reference does not match any order?
A strong reviewer blocks this change because the signature check fails open: without the secret, anyone can forge a "paid" event, and with it, genuine events from the provider are rejected.
- Empty secret by default (line 13, critical). If the variable is missing, the key is
"", and anyone can compute a valid HMAC with an empty key and mark any order as paid without paying. Fail at start-up when the secret is missing:os.environ["PAYMENTS_WEBHOOK_SECRET"]. - Signature checked against re-serialised JSON (line 26, major). The provider signed its raw body, but
json.dumpsadds spaces after:and,and may escape characters differently, so genuine events fail with 401 and orders are never marked paid. Verifyrequest.get_data()before parsing it. - Comparison is not constant-time (line 19, minor).
==stops at the first wrong character, which leaks how much of a guessed signature is right. Usehmac.compare_digest(expected, signature). - Duplicate deliveries are processed again (lines 30–36, major). The provider can send the same event more than once, so the customer gets two receipts, and a late duplicate can set a refunded order back to paid. Record each event
idand ignore ones already handled, and skip orders that are already paid. - Unknown order crashes the handler (lines 32–33, minor). If no order matches,
orderisNoneand the handler raises, returning 500. The provider then retries for three days. Log it and return 2xx, or return 404 deliberately.