Signed once, processed twice

A fictional billing webhook accepts a tampered payload and applies a valid event twice. Repair the verification contract and explain how durable processing would differ.

AppSec · about 75 minutes. Fictional evidence; Python 3.10+ for local coding.

Prerequisites

Evidence

delivery.log

10:00:00 event=evt-42 signature=valid decision=accepted credits=1
10:00:03 event=evt-42 signature=valid decision=accepted credits=2
10:00:05 event=evt-43 body_changed=true signature=unchecked decision=accepted
These are synthetic observations, not a real payment provider log.

verification-contract.txt

Fictional training contract, not vendor-specific:
HMAC-SHA256(secret, ASCII(timestamp) + b"." + exact_raw_body_bytes)
Compare the hexadecimal digest with a constant-time comparison.
Reject timestamps more than 300 seconds away from the trusted current time.
Verify integrity and freshness before parsing or recording an event.
Event id must be a non-empty string of at most 64 characters.
A repeated verified event is acknowledged as duplicate with no second effect.
Rejected messages must not consume an event id.

worker-design.txt

Current demo: JSON parse -> credit account -> remember event id.
Two workers can read "not seen" at the same time.
Required production design: durable event identity and atomic state transition.
For a database-local credit, a unique inbox row and credit update can share one transaction.
For an external side effect, design an outbox/idempotent consumer and recovery path.
A Python set demonstrates only serial deduplication, not concurrency safety.

Tasks and expected evidence

  1. Explain why valid signatures did not stop the first two deliveries. Identify the evidence for a separate integrity failure.
  2. Edit webhook_event in the exercise. Authenticate the exact raw bytes, enforce freshness, validate the event ID, and suppress serial duplicates.
  3. Run the webhook suite without --reference. Document the changed-body, wrong-signature, stale, future, malformed, and replay results.
  4. Sketch a durable inbox/outbox or transaction design. Describe a two-worker race and a crash between committing a result and acknowledging delivery.

Run the coding exercise

Download the Python exercise. It uses only the standard library and no network.

python expert_security_lab.py --lab webhook
python expert_security_lab.py --lab webhook --reference

Starter mode initially fails. Edit the matching function and rerun without --reference. Reference mode tests supplied code, not your own fix.

Limits

The reference is a serial, in-memory model. It does not guarantee concurrent or durable exactly-once delivery. Real integrations must follow their provider’s signature and retry contract.

All expert cases