Skip to content

fix: Separate an unreadable webhook payload from a forged one - #644

Merged
razor-x merged 2 commits into
mainfrom
claude/python-sdk-audit-o2iid9-11-webhook
Aug 28, 2026
Merged

fix: Separate an unreadable webhook payload from a forged one#644
razor-x merged 2 commits into
mainfrom
claude/python-sdk-audit-o2iid9-11-webhook

Conversation

@razor-x

@razor-x razor-x commented Aug 27, 2026

Copy link
Copy Markdown
Member

What

SeamWebhook.verify conflated every failure with forgery — and had zero tests (SDK audit finding M5; ports PHP #469).

Before this PR, a correctly-signed but unparseable payload leaked a raw json.JSONDecodeError from inside svix, a signed non-event payload (null, [1], {}) crashed with AttributeError in seam_event_from_dict, and the README's except Exception: return 400 mapped all of it to a signature failure — so Svix retried a permanently-bad payload across its full backoff schedule while operators investigated a phantom forgery.

  • New SeamInvalidWebhookPayloadError(SeamError): raised only after verification succeeds, for a payload that is not valid JSON or does not contain a Seam event (event_id/event_type). Genuinely from Seam, permanently unreadable — a bug report, not a retry.
  • SeamWebhookVerificationError becomes a real class subclassing both svix's WebhookVerificationError and SeamError (it was a bare re-export alias of the svix type, outside the SDK hierarchy). verify re-raises svix failures as it, with raise ... from. Fully backward compatible: except clauses on the old alias or on svix's type still catch it — and it now also satisfies except SeamError.
  • Header normalization stringifies keys and detects conflicting duplicates after lowercasing (svix-id vs SVIX-ID with different values → verification error; identical values dedupe). A malformed signature/timestamp header that leaks a bare ValueError out of the verifier is mapped to a verification error too.
  • A payload with an unknown event_type still parses to the forward-compatible fallback, unchanged.
  • README: the Flask example now distinguishes the two errors instead of except Exception, with guidance on which to let Svix retry.

Depends on #634 (SeamError base; contains its commit).

Testing

New test/seam_webhook_test.py — the first webhook tests in this SDK (19 tests), minting real signatures with svix's own Webhook.sign: valid event; mixed-case headers; tampered payload; wrong secret; expired timestamp; each missing header; conflicting and identical duplicate headers; malformed signature header; signed-but-unparseable JSON; signed non-events (null, [1], 42, "event", {}); unknown event type fallback; hierarchy assertions.

Revert check: with the old seam_webhook.py, the tests fail with the audit's exact symptoms — raw json.decoder.JSONDecodeError and AttributeError: 'NoneType' object has no attribute 'get'.

Full suite: 206 passed; mypy, pylint (10.00), black, rstcheck clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y1RzepycXEYA3LStfjt8cY


Generated by Claude Code

@razor-x
razor-x requested a review from a team as a code owner August 27, 2026 21:54
@razor-x
razor-x force-pushed the claude/python-sdk-audit-o2iid9-11-webhook branch 2 times, most recently from 0e85c8f to daca433 Compare August 28, 2026 20:43
@razor-x
razor-x merged commit 0694c69 into main Aug 28, 2026
23 checks passed
@razor-x
razor-x deleted the claude/python-sdk-audit-o2iid9-11-webhook branch August 28, 2026 22:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants