Files
pocketpaw/tests/test_web_server_security.py
Divyanshu Sharma 4717c6fe81 fix(security): redact bot token from /setup error response (#486)
Sanitize exception messages in the web_server.py /setup endpoint to
prevent the Telegram bot token from leaking in HTTP error responses.

python-telegram-bot exceptions can embed the full bot token in API
URLs (e.g. https://api.telegram.org/bot<TOKEN>/getMe). Before this
fix, str(e) was returned verbatim in the JSON response body, exposing
the token to anyone who can see the response.

The fix replaces any occurrence of the bot_token in the error message
with [REDACTED] before including it in the response. The logger.error
call is unaffected because the existing SecretFilter already scrubs
known token patterns from log output.

Addresses finding F-07 from security audit issue #445.

Co-authored-by: Rohit Kushwaha <rohitk290106@gmail.com>
2026-03-07 20:11:15 +05:30

100 lines
3.5 KiB
Python

"""Tests for web_server.py security hardening.
Covers:
- F-07 (#445): Bot token must not leak in /setup error responses.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@pytest.fixture
def _fake_settings():
"""Create a minimal mock Settings for web_server.create_app."""
s = MagicMock()
s.telegram_bot_token = ""
s.openai_api_key = ""
s.anthropic_api_key = ""
s.allowed_user_id = 0
s.save = MagicMock()
return s
class TestSetupTokenRedaction:
"""Ensure the /setup endpoint never leaks the bot token in error messages."""
def test_bot_token_redacted_from_error_response(self, _fake_settings):
"""If bot.get_me() fails with a message that contains the token,
the HTTP response must not expose it."""
fake_token = "123456:AAFakeToken-TestOnly_1234567890abcdef"
# Simulate the error python-telegram-bot raises when the API call fails;
# the URL in the error message contains the full bot token.
api_error_msg = (
f"Conflict: terminated by other getUpdates request; "
f"make sure that only one bot instance is running. "
f"URL: https://api.telegram.org/bot{fake_token}/getMe"
)
with (
patch("pocketpaw.web_server.Application") as mock_app_cls,
):
mock_builder = MagicMock()
mock_app_cls.builder.return_value = mock_builder
mock_builder.token.return_value = mock_builder
mock_app = MagicMock()
mock_builder.build.return_value = mock_app
mock_app.bot.get_me = AsyncMock(side_effect=Exception(api_error_msg))
from pocketpaw.web_server import create_app
app = create_app(_fake_settings)
from starlette.testclient import TestClient
client = TestClient(app)
resp = client.post(
"/setup",
data={"bot_token": fake_token},
)
body = resp.json()
assert "error" in body
# The raw token must NOT appear anywhere in the response
assert fake_token not in body["error"], "Bot token leaked in /setup error response"
# The redaction marker should be present instead
assert "[REDACTED]" in body["error"]
def test_error_without_token_passes_through(self, _fake_settings):
"""If the error message does not contain the token, it should still
be returned (no false-positive redaction)."""
fake_token = "123456:AAFakeToken-TestOnly_1234567890abcdef"
generic_error = "Network is unreachable"
with (
patch("pocketpaw.web_server.Application") as mock_app_cls,
):
mock_builder = MagicMock()
mock_app_cls.builder.return_value = mock_builder
mock_builder.token.return_value = mock_builder
mock_app = MagicMock()
mock_builder.build.return_value = mock_app
mock_app.bot.get_me = AsyncMock(side_effect=Exception(generic_error))
from pocketpaw.web_server import create_app
app = create_app(_fake_settings)
from starlette.testclient import TestClient
client = TestClient(app)
resp = client.post(
"/setup",
data={"bot_token": fake_token},
)
body = resp.json()
assert "error" in body
assert generic_error in body["error"]
assert "[REDACTED]" not in body["error"]