Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions python/svix/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ class WebhookVerificationError(Exception):
class Webhook:
_SECRET_PREFIX: str = "whsec_"
_whsecret: bytes
_whtolerance: timedelta

def __init__(self, whsecret: t.Union[str, bytes]):
def __init__(self, whsecret: t.Union[str, bytes], whtolerance: timedelta = timedelta(minutes=5)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a better approach would be adding the VerifyIgnoringTimestamp method, like we have in Go, as it was discussed in #93

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern with this is that it might make it 'easy' to use potentially insecure tolerances. With a separate method it's clear that the security implications are different.

Copy link
Author

@ogenodisho ogenodisho Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @svix-lucho, thanks for the review.

I did see that part in the related issue about verifying the webhook while ignoring the timestamp. However, my reasoning for doing it this way is that I think it's actually safer (and more flexible) than allowing consumers to disable the timestamp verification entirely.

If a consumer disables it entirely, they're much more susceptible to replay attacks. However, if the tolerance is configurable, they have the flexibility of tuning the tolerance to suit their use cases, while still being protected against potential replay attacks to a certain degree.

In regards to your second message, I wouldn't be opposed to introducing a verify_using_custom_tolerance method instead.

Please let me know your thoughts.

if not whsecret:
raise RuntimeError("Secret can't be empty.")

Expand All @@ -30,6 +31,8 @@ def __init__(self, whsecret: t.Union[str, bytes]):

if isinstance(whsecret, bytes):
self._whsecret = whsecret

self._whtolerance = whtolerance

def verify(self, data: t.Union[bytes, str], headers: t.Dict[str, str]) -> t.Any:
data = data if isinstance(data, str) else data.decode()
Expand Down Expand Up @@ -67,15 +70,14 @@ def sign(self, msg_id: str, timestamp: datetime, data: str) -> str:
return f"v1,{base64.b64encode(signature).decode('utf-8')}"

def __verify_timestamp(self, timestamp_header: str) -> datetime:
webhook_tolerance = timedelta(minutes=5)
now = datetime.now(tz=timezone.utc)
try:
timestamp = datetime.fromtimestamp(float(timestamp_header), tz=timezone.utc)
except Exception:
raise WebhookVerificationError("Invalid Signature Headers")

if timestamp < (now - webhook_tolerance):
if timestamp < (now - self._whtolerance):
raise WebhookVerificationError("Message timestamp too old")
if timestamp > (now + webhook_tolerance):
if timestamp > (now + self._whtolerance):
raise WebhookVerificationError("Message timestamp too new")
return timestamp
15 changes: 15 additions & 0 deletions python/tests/test_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,18 @@ def test_sign_function():
wh = Webhook(key)
signature = wh.sign(msg_id=msg_id, timestamp=timestamp, data=payload)
assert signature == expected


def test_default_tolerance_is_five_minutes():
testPayload = PayloadForTesting()
wh = Webhook(testPayload.secret)

assert wh._whtolerance == timedelta(minutes=5)


def test_custom_tolerance_is_set():
testPayload = PayloadForTesting()
custom_tolerance = timedelta(minutes=10)
wh = Webhook(testPayload.secret, custom_tolerance)

assert wh._whtolerance == custom_tolerance