# pytonapi has a Webhook Custom Path Authentication Bypass

pytonapi's webhook dispatcher stores bearer tokens only for default paths, so custom path= handlers skip auth entirely and accept forged events.

- Payout: Research
- Severity: high
- Program: pytonapi
- Found by: EQSTLab
- Disclosed: 2026-07-28
- CVE: CVE-2026-54635
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-3fcr-jvgp-7f58
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-3fcr-jvgp-7f58
- Tags: area:web domain:oss domain:web impact:integrity kind:research platform:ghsa sev:high tech:python technique:source-review vuln:access-control vuln:auth-bypass

**Target:** Python webhook dispatcher (TonapiWebhookDispatcher) HTTP endpoint
**Root cause:** setup() registers bearer tokens only under default suffix paths, never the custom path= path; process() then does self._tokens.get(path) which returns None for custom paths, and the guard `if expected_token is not None and ...` fails open, skipping authentication.
**Steps:**
1. Register a handler with the documented custom path: `@dispatcher.account_tx("0:victim", path="/hook/custom")`
2. Call `await dispatcher.setup()`; observe `_tokens` contains only `/hook/account-tx`, not `/hook/custom`
3. POST forged payload to `/hook/custom` with no Authorization header: `{"event_type":"account_tx","account_id":"0:victim","lt":1,"tx_hash":"FORGED_TX_HASH"}`
4. Handler fires with attacker-controlled tx_hash; repeat with `Authorization: Bearer totally-wrong-token` — still accepted
5. Control: same request to `/hook/account-tx` without auth correctly raises TONAPIError
**How found:** Code review of dispatcher.py: noticed setup() writes tokens keyed by `self._path + suffix` while handler registration preserves the custom `path=`, then traced _build_path_map routing the custom path to the event type and saw the fail-open `expected_token is not None` guard in process().
**Exploitation:** Unauthenticated network attacker POSTs arbitrary JSON to the victim's custom webhook path; because expected_token is None the auth check is skipped and the victim-defined handler is invoked with fully attacker-controlled event data (e.g. fake account_tx), enabling forged payment/state/notification logic.
**Insight:** When a library keys security state (tokens) by one identifier but routes requests by another (custom path), a missing-key lookup that is treated as 'no auth required' silently disables auth — grep for `.get(` on auth maps guarded by `is not None` rather than deny-by-default.

## Proof of concept (copied exactly from the original report)

**Forged unauthenticated HTTP request to custom webhook path**

```
POST /hook/custom HTTP/1.1
Host: victim.example
Content-Type: application/json
# No Authorization header

{"event_type":"account_tx","account_id":"0:victim","lt":1,"tx_hash":"FORGED_TX_HASH"}
```

