Skip to content

Validate and escape USSD codes before building AT+CUSD commands #25

Description

@Justinabox

Summary

ATCommand.ussd_send() interpolates the caller-provided USSD code directly into an AT+CUSD=1,"...",<encoding> command without validation or escaping. The HTTP /ussd/send endpoint passes request JSON code straight into modem.ussd.send(), so an authenticated API caller (or any caller while auth is disabled by default; see #4) can put quotes or CR/LF into the command string that is written to the modem.

For a Raspberry Pi PBX/SMS appliance, USSD is a network-facing control surface for balance/carrier menus; it should be fail-closed like phone numbers and SMS indexes already are.

Affected files / lines

  • server.py:125-137/ussd/send accepts data.get("code") and forwards it to modem.ussd.send(...).
  • callstack/ussd.py:53-55USSDService.send() calls ATCommand.ussd_send(code).
  • callstack/protocol/commands.py:112-115ussd_send() builds AT+CUSD=1,"{code}",{encoding} with no validation/escaping.
  • Contrast: callstack/protocol/commands.py:5-12, :57-58, and :65-80 validate phone numbers and SMS indexes before command construction.

Evidence

Baseline is healthy:

git diff --check -> exit 0
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
311 passed in 4.03s

Minimal safe reproduction (does not contact hardware):

PYTHONPATH=. uv run --no-project --with pyserial-asyncio python /tmp/callstack_issue_scout_repros.py

Relevant output:

USSD builder normal: AT+CUSD=1,"*100#",15
USSD builder with quote: 'AT+CUSD=1,"*100#,1"",15'
USSD builder with CRLF: 'AT+CUSD=1,"*100#\r\nAT+CMGD=1,4",15'

The generated CR/LF variant is especially risky because ATCommandExecutor.execute() later writes the command string plus its own trailing \r\n; embedded CR/LF should never be accepted inside a modem command parameter.

Expected behavior

USSD command construction should accept only valid USSD/menu input and should reject or safely encode any character that can break out of the quoted AT parameter. At minimum, reject quotes, carriage returns, line feeds, and unsupported DCS values before anything is written to the modem.

Actual behavior

ATCommand.ussd_send() returns command strings containing raw quotes and embedded CR/LF from caller input.

Duplicate check

No existing issues/PRs found with these searches:

gh issue list --repo Justinabox/Callstack --state all --search 'USSD quote injection CUSD command injection in:title,body'
gh issue list --repo Justinabox/Callstack --state all --search 'AT+CUSD validation escaping USSD code in:title,body'
gh pr list --repo Justinabox/Callstack --state all --search 'USSD quote injection CUSD validation escaping'

Suggested fix direction

  • Add a dedicated USSD validator/encoder in ATCommand.ussd_send() rather than relying on callers.
  • Reject CR/LF and quote characters, and constrain encoding to supported integer values.
  • Add HTTP/API tests that malicious code values return 400 (or a clear validation exception at service level) without writing to the transport.
  • Consider documenting the accepted USSD code/menu-response character set.

Acceptance criteria

  • ATCommand.ussd_send('*100#') still produces AT+CUSD=1,"*100#",15.
  • Inputs containing ", \r, or \n raise ValueError / return HTTP 400 before any modem write.
  • /ussd/send maps validation failures to a non-5xx client error with no secrets in logs.
  • Tests cover both command-builder and HTTP endpoint behavior.

Verification gates

git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/test_ussd.py tests/test_api_auth.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions