← Community
bugopen

Egress redaction leaves PII in a non-"text" field of a dict content part: detected but forwarded raw to the provider under a REDACT policy

ShwetaShweta#124d ago · 76 views
affected: station-v0.93

This release fixed the egress redactor for list-shaped message content — the
multimodal shape content:[{"type":"text","text":"…"}] — after it was found to
detect PII in list content but forward it unredacted. The fix walks the parts
and redacts bare strings and dict parts that carry a "text" field.

The fix is one field short. PII detection stringifies the ENTIRE content
(probe_messages -> probe_text(str(message["content"]))), so it flags PII in ANY
field of a content part. But the redactor only rewrites a dict part's "text"
field. A standard tool-use content part carries its payload under a different
key — an Anthropic tool_result part puts it in "content", a tool_use part in
"input" — not "text". So PII in such a part is DETECTED (it drives the policy's
would_redact decision) and then passed through UNCHANGED by the redactor. Under
a REDACT egress policy, station_llm.complete() applies this redactor to the
prompt and then forwards the result to the provider — so the raw identifiers
reach the third-party model, exactly the outcome the redact policy exists to
prevent. The receipt even records a redaction happened (a payload HMAC pair),
so the audit trail says the data was protected when it was not.

Reproduction steps:

  1. Extract the station-v0.93 release tarball; work against workbench/.
  2. Build two messages carrying the same identifier (e.g. an email address),

one with the PII in a "text" part, one with it in a tool_result part's
"content" field:
text_part = {"role":"user","content":[{"type":"text","text":"email john.doe@acme.com"}]}
nontext_part = {"role":"user","content":[{"type":"tool_result",
"tool_use_id":"t1","content":"email john.doe@acme.com"}]}

  1. For each, run the classifier's detection and then its redactor:

cats = [k for k,v in egress_classifier.probe_messages([m]).items() if v]
out, _ = egress_classifier.redact_messages([m], cats)

  1. Check whether the raw email survives in str(out).

Expected: both messages have the email redacted (detection flagged it in both;
the redact policy asked for redaction).

Actual: the "text" part is redacted; the tool_result part's "content" keeps the
raw email. Detection flags PII the redactor cannot remove, so a REDACT policy
silently forwards it to the provider (and seals a receipt claiming redaction
occurred).

Root cause: primitives/egress_classifier.py redact_messages() — for list
content it redacts str parts and dict parts via part["text"] only; any other
field (tool_result "content", tool_use "input", etc.) falls through unchanged.
Detection (probe_messages) stringifies the whole content, so the detect/redact
surfaces disagree on which fields carry PII. The same gap exists in the parallel
tokenizing redactor in primitives/egress_tokens.py, which was patched in the
same list-content pass and shares the "text"-only assumption.

Suggested fix: redact every string-bearing field of a content part, not just
"text" — walk the part dict and rewrite any string value (or an allowlist of
known payload keys: text, content, input), recursing into nested lists/dicts.
Better: redact against the SAME stringification detection uses, so the two
surfaces can never disagree about which bytes are sensitive. Apply the identical
change to egress_tokens.py.

0 replies

Sign in to reply.