diff --git a/src/gr4vy/webhooks.py b/src/gr4vy/webhooks.py index e5534200..58eb66a4 100644 --- a/src/gr4vy/webhooks.py +++ b/src/gr4vy/webhooks.py @@ -35,7 +35,10 @@ def verify_webhook( hashlib.sha256 ).hexdigest() - if expected_signature not in signatures: + # Compare in constant time, so the check doesn't leak how much of one matched. + # Bytes, because compare_digest raises TypeError on non-ASCII strings. + expected = expected_signature.encode() + if not any(hmac.compare_digest(expected, signature.encode()) for signature in signatures): raise ValueError("No matching signature found") if timestamp_tolerance and timestamp < time.time() - timestamp_tolerance: diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py index caf2d01f..3789b0b0 100644 --- a/tests/test_webhooks.py +++ b/tests/test_webhooks.py @@ -65,3 +65,32 @@ def test_verify_webhook_missing_timestamp_header(): with pytest.raises(ValueError, match="Missing header values"): verify_webhook(payload, secret, signature_header, timestamp_header, timestamp_tolerance) + + +def test_verify_webhook_accepts_a_valid_signature_in_any_position(): + secret = "Ik4L-8FH0ihWczctcIPXZRR_8F0fPNgmhEfVBbZ3zNwqQVa1Or4tBz4Pgw2eNaVDod7H56Y268h_wohEUaWbUg" + signature_header = "other,78aca0c78005107a654a957b8566fa6e0e5e06aea92d7da72a6da9e5a690d013" + + verify_webhook("payload", secret, signature_header, "1744018920", 0) + + +def test_verify_webhook_rejects_a_nearly_matching_signature(): + secret = "Ik4L-8FH0ihWczctcIPXZRR_8F0fPNgmhEfVBbZ3zNwqQVa1Or4tBz4Pgw2eNaVDod7H56Y268h_wohEUaWbUg" + signature_header = "78aca0c78005107a654a957b8566fa6e0e5e06aea92d7da72a6da9e5a690d014" + + with pytest.raises(ValueError, match="No matching signature found"): + verify_webhook("payload", secret, signature_header, "1744018920", 0) + + +def test_verify_webhook_rejects_a_non_ascii_signature(): + secret = "Ik4L-8FH0ihWczctcIPXZRR_8F0fPNgmhEfVBbZ3zNwqQVa1Or4tBz4Pgw2eNaVDod7H56Y268h_wohEUaWbUg" + + with pytest.raises(ValueError, match="No matching signature found"): + verify_webhook("payload", secret, "é", "1744018920", 0) + + +def test_verify_webhook_accepts_a_valid_signature_after_a_non_ascii_one(): + secret = "Ik4L-8FH0ihWczctcIPXZRR_8F0fPNgmhEfVBbZ3zNwqQVa1Or4tBz4Pgw2eNaVDod7H56Y268h_wohEUaWbUg" + signature_header = "é,78aca0c78005107a654a957b8566fa6e0e5e06aea92d7da72a6da9e5a690d013" + + verify_webhook("payload", secret, signature_header, "1744018920", 0)