Skip to content

Sanitize peer label before rendering into notification line (SEC-001) - #8

Open
agigante80 wants to merge 2 commits into
yilunzhang:mainfrom
agigante80:fix/sec-001-label-header-spoofing
Open

Sanitize peer label before rendering into notification line (SEC-001)#8
agigante80 wants to merge 2 commits into
yilunzhang:mainfrom
agigante80:fix/sec-001-label-header-spoofing

Conversation

@agigante80

Copy link
Copy Markdown

Closes #6.

Problem

A peer-controlled label was interpolated raw into the single-line stdout notification the receiving session's LLM is instructed to act on:

[inter-session msg=<id> from="<name>" "<label>"] <text>

name is ASCII-locked by NAME_RE and text goes through sanitize_for_stdout, but label passed through neither. validate_label permitted " [ ], so a crafted label could close its quoted field and reconstruct a second, forged [inter-session … from="…"] header — spoofing the sender. The same raw label was also written to messages.log (_log_message), which the truncated-message flow greps and shows to the agent (and json.dumps does not escape [/]).

Note: cwd — another peer-controlled string rendered on the same surfaces — is already sanitized server-side before storage (server.py, with a "terminal-escape injection by a hostile peer" comment). label was the same pattern, missed.

Fix

  • Boundary reject (primary): add LABEL_FORBIDDEN_CHARS = {" ' [ ] "} and reject them in validate_label. One check covers every surface that reflects a label — notification line, list table, messages.log, and any future consumer — instead of relying on each render site to remember to escape.
  • Render-time neutralization (defense-in-depth): sanitize_label_for_display strips control/ANSI, folds tabs to spaces (so a tab can't disrupt the list table's fixed-width columns), and neutralizes " [ ] to safe look-alikes. Applied in _format_msg (client.py) and the list renderer (list.py) for any label that never passed validation.

Legitimate labels (spaces, Unicode, emoji) are unaffected.

Tests

  • tests/test_shared.pyvalidate_label rejects " [ ] and a full forged-header string.
  • tests/test_client.py_format_msg with a malicious label yields only one [inter-session token and no forged from="…".

Full suite: 197 passed. (The 4 failing test_helpers/test_client two-listener tests are a pre-existing server-election race, unrelated to this change.)

Companion

SEC-002 (#7) — a reaction-policy note that only the leading notification header is authoritative — is submitted as a separate PR.

Found via a security review of the runtime source.

agigante80 and others added 2 commits July 12, 2026 10:11
A peer-controlled `label` was interpolated raw into the single-line
stdout notification the receiving session's LLM acts on:

    [inter-session msg=<id> from="<name>" "<label>"] <text>

`name` is ASCII-locked and `text` is control-char sanitized, but
`label` passed through neither. `validate_label` permits `"`, `[`, `]`,
so a crafted label could close its quoted field and reconstruct a
second `[inter-session ... from="..."]` header, spoofing the sender
to the receiving agent (which is instructed to act on messages).

Add `shared.sanitize_label_for_display`, which strips control/ANSI and
neutralizes the header-structural characters `"`, `[`, `]`, and route
`from_label` through it in both render sites (client `_format_msg` and
`list` table output). This mirrors the existing server-side `cwd`
sanitization. Legitimate labels (spaces, Unicode, emoji) are
unaffected; regression test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WwNCo3qLamBCzhMVAHGuH9
…low-up)

Code-review follow-up to the render-time neutralization: the raw label
was still reachable unsanitized via messages.log — server._log_message
writes from_label verbatim, and json.dumps does not escape `[`/`]`, so a
forged `[inter-session ...]` fragment survived into the log that the
truncated-message flow greps and shows to the receiving agent.

Add LABEL_FORBIDDEN_CHARS = {" ' [ ] "} and reject them in validate_label.
This is a single boundary defense that covers every surface reflecting a
label (notification line, list table, messages.log, future consumers),
instead of relying on each render site to remember to neutralize. The
render-time sanitize_label_for_display stays as belt-and-suspenders for
labels that never passed validation, and now also folds tabs (which
sanitize_for_stdout preserves) so they can't disrupt the list table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WwNCo3qLamBCzhMVAHGuH9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SEC-001: peer label not escaped before rendering into notification line (sender spoofing)

1 participant