mirror of
https://github.com/pocketpaw/pocketpaw.git
synced 2026-05-19 08:26:34 +00:00
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>
100 lines
3.5 KiB
Python
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"]
|