From c1dc3e77e0a68a798fdc76026d5350604bc8b721 Mon Sep 17 00:00:00 2001 From: CartSnitch Engineer Bot Date: Fri, 3 Apr 2026 12:09:51 +0000 Subject: [PATCH] fix(receiptwitness): handle invalid timestamp in Mailgun webhook verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap int(timestamp) in try/except to return False instead of raising ValueError on empty/invalid timestamp, which was causing a 500 error instead of the intended 406. Also add tests for empty timestamp (→ 406) and GET /inbound/email (→ 405). Co-Authored-By: Paperclip --- .../src/receiptwitness/api/routes.py | 6 ++++- receiptwitness/tests/test_api/test_webhook.py | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/receiptwitness/src/receiptwitness/api/routes.py b/receiptwitness/src/receiptwitness/api/routes.py index 483cdcc..2437bcc 100644 --- a/receiptwitness/src/receiptwitness/api/routes.py +++ b/receiptwitness/src/receiptwitness/api/routes.py @@ -17,7 +17,11 @@ TOKEN_PATTERN = re.compile(r"receipts\+([A-Za-z0-9_-]+)@") def verify_mailgun_signature(token: str, timestamp: str, signature: str) -> bool: """Verify Mailgun webhook signature.""" - if abs(time.time() - int(timestamp)) > 300: # 5 min freshness + try: + ts = int(timestamp) + except (ValueError, TypeError): + return False + if abs(time.time() - ts) > 300: # 5 min freshness return False key = settings.mailgun_webhook_signing_key.encode() hmac_digest = hmac.new(key, f"{timestamp}{token}".encode(), hashlib.sha256).hexdigest() diff --git a/receiptwitness/tests/test_api/test_webhook.py b/receiptwitness/tests/test_api/test_webhook.py index 2b208de..164144a 100644 --- a/receiptwitness/tests/test_api/test_webhook.py +++ b/receiptwitness/tests/test_api/test_webhook.py @@ -99,3 +99,27 @@ def test_stale_timestamp(client, mock_redis): assert response.status_code == 406 assert response.json()["detail"] == "Invalid signature" mock_redis["enqueue"].assert_not_awaited() + + +def test_invalid_timestamp_returns_406(client, mock_redis): + """Empty timestamp should return 406, not 500.""" + with patch("receiptwitness.api.routes.settings") as mock_settings: + mock_settings.mailgun_webhook_signing_key = "test-secret" + form = { + "token": "test-token", + "timestamp": "", + "signature": "any-sig", + "sender": "sender@example.com", + "recipient": "receipts+user123@example.com", + "subject": "Receipt", + } + response = client.post("/inbound/email", data=form) + assert response.status_code == 406 + assert response.json()["detail"] == "Invalid signature" + mock_redis["enqueue"].assert_not_awaited() + + +def test_get_inbound_email_returns_405(client): + """GET /inbound/email is not allowed.""" + response = client.get("/inbound/email") + assert response.status_code == 405