Webhook Custom Path Authentication Bypass in pytonapi
Summary
TonapiWebhookDispatcher in pytonapi 2.2.0 fails to validate the Authorization header when a webhook handler is registered with the documented path= argument. During setup(), bearer tokens are stored only under the default suffix paths (e.g., /hook/account-tx), but the custom path (e.g., /hook/custom) is never added to the token map. When an incoming request arrives at the custom path, self._tokens.get(path) returns None, causing the if expected_token is not None guard to evaluate to False and silently skip authentication entirely. An unauthenticated remote attacker can POST arbitrary forged payloads to the custom webhook endpoint and trigger victim-defined handlers with full integrity impact.
Details
The vulnerability is a fail-open authentication check in pytonapi/webhook/dispatcher.py.
Token registration (setup) stores tokens only under default suffix paths:
# dispatcher.py lines 109-112
suffix = self.DEFAULT_SUFFIXES[event_type]
local_path = self._path + suffix # e.g., "/hook/account-tx"
webhook = await self._client.ensure(f"{self._url}{suffix}")
self._tokens[local_path] = webhook.token # custom path is NEVER stored here
Handler registration preserves the custom path in the handler tuple:
# dispatcher.py lines 339, 342
resolved_path = path or self._resolve_path(event_type) # -> "/hook/custom"
self._handlers[event_type].append((account_filter, fn, resolved_path))
Path routing (_build_path_map) correctly maps the custom path to the event type:
# dispatcher.py line 182
return {handlers[0][2]: et for et, handlers in self._handlers.items() if handlers}
# -> {"/hook/custom": WebhookEventType.ACCOUNT_TX}
Authentication check (fail-open):