diff --git a/resend/version.py b/resend/version.py index 9ada10a..4ccbcac 100644 --- a/resend/version.py +++ b/resend/version.py @@ -1,4 +1,4 @@ -__version__ = "2.49.0" +__version__ = "2.49.1" def get_version() -> str: diff --git a/resend/webhooks/_webhooks.py b/resend/webhooks/_webhooks.py index 3a64cb3..8dc601b 100644 --- a/resend/webhooks/_webhooks.py +++ b/resend/webhooks/_webhooks.py @@ -603,7 +603,11 @@ def verify(cls, options: VerifyWebhookOptions) -> WebhookEventPayload: continue received_signature = parts[1] - if hmac.compare_digest(expected_signature, received_signature): + # Compare bytes: str comparison raises TypeError on non-ASCII input + if hmac.compare_digest( + expected_signature.encode("utf-8"), + received_signature.encode("utf-8", "replace"), + ): try: return cast(WebhookEventPayload, json.loads(options["payload"])) except json.JSONDecodeError as e: diff --git a/tests/webhooks_test.py b/tests/webhooks_test.py index 30fc0b9..c53dde3 100644 --- a/tests/webhooks_test.py +++ b/tests/webhooks_test.py @@ -309,6 +309,41 @@ def test_verify_invalid_signature(self) -> None: with pytest.raises(ValueError, match="no matching signature found"): resend.Webhooks.verify(options) + def test_verify_non_ascii_signature(self) -> None: + """Test webhook verification rejects a non-ASCII signature with ValueError""" + secret = "whsec_" + base64.b64encode(b"test_secret_key").decode("utf-8") + + options: resend.VerifyWebhookOptions = { + "payload": '{"type":"email.sent","data":{"email_id":"123"}}', + "headers": { + "id": "msg_123", + "timestamp": str(int(time.time())), + "signature": "v1,\u00e9\u00e8", + }, + "webhook_secret": secret, + } + + with pytest.raises(ValueError, match="no matching signature found"): + resend.Webhooks.verify(options) + + def test_verify_unpaired_surrogate_signature(self) -> None: + """Test webhook verification rejects a signature with an unpaired surrogate with ValueError""" + secret = "whsec_" + base64.b64encode(b"test_secret_key").decode("utf-8") + + options: resend.VerifyWebhookOptions = { + "payload": '{"type":"email.sent","data":{"email_id":"123"}}', + "headers": { + "id": "msg_123", + "timestamp": str(int(time.time())), + # A raw 0xE9 header byte decoded with errors="surrogateescape" + "signature": "v1,\udce9", + }, + "webhook_secret": secret, + } + + with pytest.raises(ValueError, match="no matching signature found"): + resend.Webhooks.verify(options) + def test_verify_expired_timestamp(self) -> None: """Test webhook verification with expired timestamp""" secret = "whsec_" + base64.b64encode(b"test_secret_key").decode("utf-8")