CVE-2026-54635
pytonapi webhook authentication is bypassed when using a custom path.
- CVSS 7.5
- 287
- Authentication, Authorization, and Session Management
- Remote
pytonapi is a Python SDK for TONAPI that provides REST API, streaming, and webhook access to the TON blockchain. From 2.0.0 to 2.2.0, TonapiWebhookDispatcher fails to validate the Authorization header when a webhook handler is registered with the documented path argument, because setup() stores bearer tokens only under the default suffix paths and never adds the custom path to the token map, so self._tokens.get(path) returns None and the authentication guard is skipped. An unauthenticated remote attacker can POST forged payloads to the custom webhook endpoint and trigger victim-defined handlers. This issue is fixed in version 2.2.1.
- CWE
- 287
- CVSS base score
- 7.5
- Published
- 2026-07-28
- OWASP
- A07 Identification and Authentication Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Authentication, Authorization, and Session Management
- Subcategory
- Insecure Authentication Mechanisms
- Accessibility scope
- Remote
- Impact
- Unauthorized Access
- Affected component
- pytonapi
- Fixed by upgrading
- Yes
Solution
Upgrade `pytonapi` to version 2.2.1 or later.
Vulnerable code sample
from typing import Dict, Optional
# Mock objects for context
from aiohttp import web
class BaseHandler: ...
class MempoolHandler(BaseHandler): ...
class TransactionHandler(BaseHandler): ...
class TONAPI:
def __init__(self, api_key: str):
self.api_key = api_key
class TonapiWebhookDispatcher:
def __init__(self, tonapi: TONAPI):
self.tonapi = tonapi
self._handlers: Dict[str, BaseHandler] = {}
self._tokens: Dict[str, str] = {}
def setup(self, handler: BaseHandler, path: Optional[str] = None) -> None:
token = self.tonapi.api_key
if isinstance(handler, MempoolHandler):
webhook_path = "/v1/mempool/webhook"
self._handlers[webhook_path] = handler
self._tokens[webhook_path] = token
elif isinstance(handler, TransactionHandler):
webhook_path = "/v1/transaction/webhook"
self._handlers[webhook_path] = handler
self._tokens[webhook_path] = token
elif path:
# VULNERABLE: The custom path is used to register the handler, but the token is not stored.
self._handlers[path] = handler
async def _handle_request(self, request: web.Request) -> web.Response:
token = self._tokens.get(request.path)
if token: # This authentication check is skipped for custom paths
auth_header = request.headers.get("Authorization")
if not auth_header or auth_header != f"Bearer {token}":
raise web.HTTPUnauthorized()
handler = self._handlers.get(request.path)
if handler:
return web.Response(text="OK")
return web.Response(status=404)Patched code sample
from typing import Dict, Optional
# Mock objects for context
from aiohttp import web
class BaseHandler: ...
class MempoolHandler(BaseHandler): ...
class TransactionHandler(BaseHandler): ...
class TONAPI:
def __init__(self, api_key: str):
self.api_key = api_key
class TonapiWebhookDispatcher:
def __init__(self, tonapi: TONAPI):
self.tonapi = tonapi
self._handlers: Dict[str, BaseHandler] = {}
self._tokens: Dict[str, str] = {}
def setup(self, handler: BaseHandler, path: Optional[str] = None) -> None:
token = self.tonapi.api_key
if isinstance(handler, MempoolHandler):
path = "/v1/mempool/webhook"
elif isinstance(handler, TransactionHandler):
path = "/v1/transaction/webhook"
if path:
self._handlers[path] = handler
# FIX: The token is now stored for both default and custom paths.
self._tokens[path] = token
async def _handle_request(self, request: web.Request) -> web.Response:
token = self._tokens.get(request.path)
if token:
auth_header = request.headers.get("Authorization")
if not auth_header or auth_header != f"Bearer {token}":
raise web.HTTPUnauthorized()
handler = self._handlers.get(request.path)
if handler:
return web.Response(text="OK")
return web.Response(status=404)Payload
{
"events": [
{
"event_id": "a1b2c3d4e5f67890fakedata",
"timestamp": 1678886400,
"actions": [
{
"type": "JettonTransfer",
"status": "ok",
"JettonTransfer": {
"sender": {
"address": "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"
},
"recipient": {
"address": "UQAvs2-vJp1W1A3oP9vP5gZ_ZJ-3Z-J-3Z-J-3Z-J-3Z-J-3Z"
},
"amount": "1000000000",
"jetton_master": {
"address": "EQB-ajMyi5-WKi5-WKi5-WKi5-WKi5-WKi5-WKi5-WKi5-WKi"
}
}
}
]
}
]
}
Cite this entry
@misc{vaitp:cve202654635,
title = {{pytonapi webhook authentication is bypassed when using a custom path.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-54635},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-54635/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
