diff --git a/app/public_routes.py b/app/public_routes.py index b3ce61b..9e95834 100644 --- a/app/public_routes.py +++ b/app/public_routes.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from collections.abc import Callable from typing import Any from urllib.parse import urlencode @@ -23,6 +24,8 @@ UNSUPPORTED_PUBLIC_PATHS_SUMMARY, ) +CONTROL_CHARACTER_RE = r"[\x00-\x1f\x7f]" + def _bounties_api_url( status: str | None, query_text: str, selected_sort: str, limit: int | None @@ -66,6 +69,8 @@ def public_bounties_context( def wallets_page_context(session: Session, q: str | None = None) -> dict[str, Any]: + if q is not None and re.search(CONTROL_CHARACTER_RE, q): + raise HTTPException(status_code=400, detail="q must not contain control characters") query_text = q.strip() if q is not None else "" query = select(Wallet) if query_text: diff --git a/tests/test_wallet_api.py b/tests/test_wallet_api.py index 7ba9067..bc50d49 100644 --- a/tests/test_wallet_api.py +++ b/tests/test_wallet_api.py @@ -580,6 +580,16 @@ def test_wallet_pages_expose_transfer_and_github_claim_flows(sqlite_url: str) -> assert "Link a wallet" in me +def test_wallet_search_rejects_control_characters(sqlite_url: str) -> None: + create_schema(sqlite_url) + client = TestClient(create_app(database_url=sqlite_url, webhook_secret="secret")) + + response = client.get("/wallets", params={"q": "\t"}) + + assert response.status_code == 400 + assert "q must not contain control characters" in response.text + + def test_me_page_shows_signed_in_github_claim_balance(sqlite_url: str, monkeypatch) -> None: monkeypatch.setenv("MERGEWORK_COOKIE_SECRET", "test-cookie-secret") create_schema(sqlite_url)