API Security in Depth · Configuration, Inventory & Secure Operations
Unsafe Consumption of APIs & Webhook Security
Treat third-party APIs and inbound webhooks as untrusted input.
API10 is the mirror image: your service consumes partner APIs and receives webhooks, and trusts them too much. Validate and bound everything you receive (schema, size, redirects, timeouts), use TLS, don't follow redirects blindly, and contain failures with circuit breakers. For inbound webhooks: verify HMAC signatures over the raw body with constant-time comparison, check timestamps to prevent replay (Standard Webhooks: webhook-id, webhook-timestamp, webhook-signature), dedupe by event ID, respond fast and process asynchronously, and re-fetch the resource from the provider's API for critical state.
Inbound webhook checklist
| Check | Why |
|---|---|
| Signature over raw body (HMAC / asymmetric) | Authenticity + integrity |
| Timestamp tolerance (e.g. 5 min) | Replay protection |
| Dedupe by event/webhook ID | Providers deliver at-least-once |
| Schema validation + size limits | Malformed or malicious payloads |
| Re-fetch critical resources | Don't trust payload state blindly; ordering |
| Secret rotation (multiple valid secrets) | Zero-downtime rotation |
| Quick 2xx then async processing | Avoid provider timeouts and retries storms |
def verify(raw: bytes, headers: Mapping[str, str], secrets: list[bytes]) -> None:
msg_id, ts = headers["webhook-id"], int(headers["webhook-timestamp"])
if abs(time.time() - ts) > 300:
raise Unauthorized("stale")
signed = f"{msg_id}.{ts}.".encode() + raw
provided = [s.split(",", 1)[1] for s in headers["webhook-signature"].split() if s.startswith("v1,")]
for secret in secrets: # supports rotation
expected = base64.b64encode(hmac.new(secret, signed, hashlib.sha256).digest()).decode()
if any(hmac.compare_digest(expected, p) for p in provided):
return
raise Unauthorized("bad signature")
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(req.body, req.headers["stripe-signature"]!, endpointSecret);
} catch { return res.sendStatus(400); }
if (await processed.has(event.id)) return res.sendStatus(200); // dedupe
await queue.add("stripe-event", { id: event.id, type: event.type });
res.sendStatus(200);
});
Common pitfalls
- Verifying signatures over re-serialized JSON instead of raw bytes.
- Using
==for signature comparison. - Trusting partner responses without validation or timeouts.
Key points
- Third-party data is untrusted input
- Verify, dedupe, then process async
- Re-fetch authoritative state
Further reading: OWASP API10:2023
Practice questions
Why must webhook signatures be computed over the raw request body?
Answer
Parsing and re-serializing JSON can change whitespace/key order, producing a different byte string and either false failures or a bypassable check.
How do you protect a webhook endpoint against replay attacks?
Answer
Include a signed timestamp and reject old messages, and dedupe by the unique event/message ID.
Open in the app to mark lessons complete, take notes and track your progress.