{"repro_id":"REPRO-2026-00218","version":7,"title":"fast-mcp-telegram <=0.19.0 allows bearer token path traversal to authenticate as the default telegram.session, bypassing reserved session name protections and enabling unauthorized access to Telegram MCP tools.","repro_type":"security","status":"published","severity":"critical","cvss_score":9.4,"description":"fast-mcp-telegram validates HTTP Bearer tokens by joining the raw token string into a session-file path without normalizing or rejecting path separators. Although the exact reserved token `telegram` is blocked, traversal aliases (e.g., `../fast-mcp-telegram/telegram`) resolve to the same default session file and are accepted. A remote HTTP client can authenticate as the default legacy session when `~/.config/fast-mcp-telegram/telegram.session` exists, enabling access to Telegram MCP tools as that account.","root_cause":"# RCA Report: CVE-2026-52830 (fast-mcp-telegram path-traversal session bypass)\n\n## Summary\n\nfast-mcp-telegram <= 0.19.0 uses the raw HTTP Bearer token as a file-name fragment when building a session-file path. It checks the token against a set of reserved names (e.g. `telegram`) but never rejects path separators or normalizes the result. A token such as `../fast-mcp-telegram/telegram` therefore resolves to the same reserved `telegram.session` file that the exact name check is meant to protect, while bypassing the name check. A remote HTTP client who knows the target session directory layout can authenticate as the default account and list or invoke the exposed Telegram MCP tools.\n\n## Impact\n\n- **Package/component:** fast-mcp-telegram (PyPI), specifically `src.server_components.session_token_verifier.SessionFileTokenVerifier` and `src.server_components.auth_middleware.UrlTokenMiddleware`.\n- **Affected versions:** `<= 0.19.0`.\n- **Patched version:** `0.19.1`.\n- **Risk level:** High. A remote, unauthenticated attacker can bypass bearer-token authentication and gain access to the victim's Telegram MCP tools as the default session without any Telegram credentials.\n- **Consequences:** Unauthorized access to tools such as `get_messages`, `send_message`, `send_message_to_phone`, `invoke_mtproto`, etc., under the identity of the default `telegram` session.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Authorization bypass via HTTP Bearer token path traversal (`api_remote` / `authz_bypass`).\n- **Reproduced impact from this run:** Real HTTP auth bypass on a running `fast-mcp-telegram` 0.19.0 instance. The reserved token `telegram` is rejected (HTTP 401), while the traversal alias `../fast-mcp-telegram/telegram` is accepted (HTTP 200) and returns the protected `tools/list` response. The patched 0.19.1 build rejects the same traversal token (HTTP 401).\n- **Parity:** `full` for the claimed auth-bypass surface; the reproduction exercises the actual remote HTTP API and reaches the authenticated MCP path.\n- **Not demonstrated:** We did not demonstrate actual Telegram message exfiltration or message sending, because no real Telegram session credentials are configured. However, the auth layer is clearly bypassed and the tool list is exposed, proving the claimed authorization bypass.\n\n## Root Cause\n\nIn `src/server_components/session_token_verifier.py` (0.19.0), `verify_token()` does:\n\n```python\nif token.lower() in RESERVED_SESSION_NAMES:\n    return None\nsession_path = self._session_directory / f\"{token}.session\"\nif not session_path.is_file():\n    return None\nreturn AccessToken(token=token, ...)\n```\n\nThe reserved-name check is exact and case-insensitive, but the token string is then inserted directly into the path with only a `.session` suffix. Because `pathlib.Path` does not normalize `..` in the operand, the token `../fast-mcp-telegram/telegram` yields:\n\n```\n<session_dir>/../fast-mcp-telegram/telegram.session\n```\n\nWhen `session_dir` is the default `~/.config/fast-mcp-telegram`, this resolves to `~/.config/fast-mcp-telegram/telegram.session`, the same default session file the exact-name check is trying to protect. The same vulnerable path construction is also present in `UrlTokenMiddleware`, which rewrites the URL token into the `Authorization` header.\n\nThe 0.19.1 fix introduces `src/server_components/session_token_validation.py`. It validates the token against a strict `^[A-Za-z0-9_-]{43}$` pattern and uses `session_file_path()`, which resolves the constructed path and verifies it is still inside `session_dir` with `is_relative_to()`. Any traversal sequence or reserved name is rejected before the file existence check.\n\n## Reproduction Steps\n\nRun the self-contained script:\n\n```bash\nbash bundle/repro/reproduction_steps.sh\n```\n\nThe script:\n\n1. Reads `bundle/project_cache_context.json` and uses the provided `project_cache_dir` for persistent Python venvs.\n2. Creates two virtual environments, installing `fast-mcp-telegram==0.19.0` (vulnerable) and `==0.19.1` (fixed).\n3. Creates a controlled `HOME` and default session directory `~/.config/fast-mcp-telegram`, then touches `telegram.session` so the default session exists.\n4. Starts each version in `http-auth` mode on a different localhost port and waits for `/health` to return 200.\n5. Sends a JSON-RPC `tools/list` request to `POST /v1/mcp` with three different bearer tokens on the vulnerable server:\n   - `telegram` (reserved, expected 401)\n   - `../fast-mcp-telegram/telegram` (traversal alias, expected 200)\n   - `invalid-token` (no matching session file, expected 401)\n6. Sends the same traversal token to the fixed server (expected 401).\n7. Writes `bundle/repro/runtime_manifest.json` and exits 0 only when the expected statuses are observed.\n\n### Expected evidence\n\n- `repro/artifacts/http_vuln_reserved.txt` and `http_vuln_noauth.txt`: HTTP 401 response body.\n- `repro/artifacts/http_vuln_traversal.txt`: HTTP 200 SSE event containing the full `tools/list` result.\n- `repro/artifacts/http_fixed_traversal.txt`: HTTP 401 response body from the patched version.\n- `logs/server_vuln.log` and `logs/server_fixed.log`: server startup logs showing mode `http-auth` and the session directory.\n\n## Evidence\n\nCaptured artifacts:\n\n- `bundle/repro/artifacts/http_vuln_traversal.txt` (HTTP 200, tools list returned)\n- `bundle/repro/artifacts/http_vuln_reserved.txt` (HTTP 401, reserved token rejected)\n- `bundle/repro/artifacts/http_vuln_noauth.txt` (HTTP 401, invalid token rejected)\n- `bundle/repro/artifacts/http_fixed_traversal.txt` (HTTP 401, traversal token rejected on 0.19.1)\n- `bundle/logs/server_vuln.log`\n- `bundle/logs/server_fixed.log`\n- `bundle/repro/runtime_manifest.json`\n- `bundle/logs/reproduction_steps.log`\n- `bundle/logs/reproduction_steps_run2.log`\n\nKey excerpt from the vulnerable traversal response (first line only for brevity):\n\n```\nevent: message\\r\\n\ndata: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"tools\":[{\"name\":\"search_messages_globally\", ...\n```\n\nThis demonstrates that the protected `tools/list` endpoint returned successfully using only the traversal token.\n\nThe fixed server log confirms the same traversal token now returns HTTP 401:\n\n```\n{\"error\": \"invalid_token\", \"error_description\": \"Authentication failed...\"}\n```\n\nEnvironment details captured in the logs: Python 3.14, uvicorn, fastmcp-slim 3.4.2, fast-mcp-telegram 0.19.0 / 0.19.1, session directory `repro/fakehome/.config/fast-mcp-telegram`.\n\n## Recommendations / Next Steps\n\n- **Upgrade** to `fast-mcp-telegram >= 0.19.1`, which enforces a strict token format and resolves/session-directory containment checks.\n- **Network-level mitigation** until patched: restrict access to the MCP HTTP port so only trusted clients can reach it.\n- **Detection:** monitor authentication logs for tokens containing path separators or unusual patterns.\n- **Testing recommendation:** add regression tests that attempt tokens such as `../session`, `..\\\\session`, `telegram`, and `/etc/passwd` and assert they are rejected before any file existence check.\n\n## Additional Notes\n\n- The script was run twice consecutively from a clean project-cache state; both runs exited 0 and produced the same status sequence (401, 200, 401 on vulnerable; 401 on fixed), confirming idempotency.\n- The GitHub source repository for fast-mcp-telegram was not directly reachable in this environment, so the reproduction relies on the official PyPI wheels. The relevant source code is visible in the installed site-packages and confirms the vulnerable `Path` concatenation in 0.19.0 and the new `session_token_validation.py` containment check in 0.19.1.\n- No real Telegram credentials or network connectivity to Telegram are required for the reproduction; the bypass is demonstrated purely against the local authentication layer.\n","cve_id":"CVE-2026-52830","cwe_id":"CWE-22","source_url":"https://nvd.nist.gov/vuln/detail/CVE-2026-52830","package":{"name":"fast-mcp-telegram","ecosystem":"pip","affected_versions":"<= 0.19.0","fixed_version":"0.19.1"},"reproduced_at":"2026-07-03T15:53:44.941289+00:00","duration_secs":1071.0,"tool_calls":186,"handoffs":2,"total_cost_usd":2.2710070099999995,"agent_costs":{"hypothesis_generator":0.02510165,"judge":0.200862,"repro":0.53551782,"support":0.03539976,"vuln_variant":1.4741257799999998},"cost_breakdown":{"hypothesis_generator":{"accounts/fireworks/models/kimi-k2p7-code":0.02510165},"judge":{"gpt-5.5":0.200862},"repro":{"accounts/fireworks/models/kimi-k2p7-code":0.53551782},"support":{"accounts/fireworks/models/kimi-k2p7-code":0.03539976},"vuln_variant":{"accounts/fireworks/models/kimi-k2p7-code":1.4741257799999998}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-03T15:53:45.806609+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":7369,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":7790,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":9204,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":11252,"category":"analysis"},{"path":"bundle/ticket.md","filename":"ticket.md","size":3145,"category":"ticket"},{"path":"bundle/ticket.json","filename":"ticket.json","size":4316,"category":"other"},{"path":"bundle/repro/artifacts/http_vuln_reserved.txt","filename":"http_vuln_reserved.txt","size":301,"category":"other"},{"path":"bundle/repro/artifacts/http_vuln_traversal.txt","filename":"http_vuln_traversal.txt","size":12901,"category":"other"},{"path":"bundle/repro/artifacts/http_vuln_noauth.txt","filename":"http_vuln_noauth.txt","size":301,"category":"other"},{"path":"bundle/repro/artifacts/http_fixed_traversal.txt","filename":"http_fixed_traversal.txt","size":301,"category":"other"},{"path":"bundle/repro/fakehome/.config/fast-mcp-telegram/telegram.session","filename":"telegram.session","size":0,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":712,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":730,"category":"other"},{"path":"bundle/logs/pip_0.19.0.log","filename":"pip_0.19.0.log","size":60325,"category":"log"},{"path":"bundle/logs/pip_0.19.1.log","filename":"pip_0.19.1.log","size":57789,"category":"log"},{"path":"bundle/logs/server_vuln.log","filename":"server_vuln.log","size":1848,"category":"log"},{"path":"bundle/logs/server_fixed.log","filename":"server_fixed.log","size":1728,"category":"log"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":50304,"category":"log"},{"path":"bundle/logs/reproduction_steps_run2.log","filename":"reproduction_steps_run2.log","size":50304,"category":"log"},{"path":"bundle/logs/variant_reproduction_steps_run1.log","filename":"variant_reproduction_steps_run1.log","size":13489,"category":"log"},{"path":"bundle/logs/server_vuln_variant.log","filename":"server_vuln_variant.log","size":2135,"category":"log"},{"path":"bundle/logs/server_fixed_variant.log","filename":"server_fixed_variant.log","size":2327,"category":"log"},{"path":"bundle/logs/variant_reproduction_steps_run2.log","filename":"variant_reproduction_steps_run2.log","size":13503,"category":"log"},{"path":"bundle/vuln_variant/fakehome/.config/fast-mcp-telegram/telegram.session","filename":"telegram.session","size":0,"category":"other"},{"path":"bundle/vuln_variant/artifacts/http_vuln_reserved.txt","filename":"http_vuln_reserved.txt","size":301,"category":"other"},{"path":"bundle/vuln_variant/artifacts/http_vuln_dot_slash.txt","filename":"http_vuln_dot_slash.txt","size":12901,"category":"other"},{"path":"bundle/vuln_variant/artifacts/http_vuln_original.txt","filename":"http_vuln_original.txt","size":12901,"category":"other"},{"path":"bundle/vuln_variant/artifacts/http_vuln_invalid.txt","filename":"http_vuln_invalid.txt","size":301,"category":"other"},{"path":"bundle/vuln_variant/artifacts/http_vuln_url_dot_slash.txt","filename":"http_vuln_url_dot_slash.txt","size":74,"category":"other"},{"path":"bundle/vuln_variant/artifacts/http_fixed_dot_slash.txt","filename":"http_fixed_dot_slash.txt","size":301,"category":"other"},{"path":"bundle/vuln_variant/artifacts/http_fixed_original.txt","filename":"http_fixed_original.txt","size":301,"category":"other"},{"path":"bundle/vuln_variant/artifacts/http_fixed_url_dot_slash.txt","filename":"http_fixed_url_dot_slash.txt","size":74,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1106,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":5576,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":3280,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":1094,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":819,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1488,"category":"other"}]}