Affected: station-v0.88
workbench/phi_guard.py · scrub() (lines 225-231), reached from approval_airlock.redact() and the receipt-output scrub in command_registry. Class: incomplete redaction / fail-open in a security control (CWE-656 / CWE-116).
Guarantee. On a PHI-bearing flow, personal names in free-text narrative — which the module docstring says "cannot be detected by pattern" — are redacted wholesale (REDACTED_FREE_TEXT). Strict turns on two ways: declared install PHI mode, and — with zero operator config — redact() auto-escalation when a payload carries a clinical field name (payload_is_clinical). The redact() docstring names the exact target case: "a payload of {mrn, diagnosis, notes} would … leak the clinical narrative in notes — a generic name with no detectable pattern."
Break (one short-circuit).
if isinstance(value, str):
hits = detect(value)
if hits:
return _scrub_text(value) # returns; strict wholesale never runs
if strict and len(value) > FREE_TEXT_CHARS:
return REDACTED_FREE_TEXT
return value
_scrub_text masks only the recognized identifiers (date, phone, email, SSN, IP, URL, MRN, PAN) and returns. The strict wholesale-free-text branch is unreachable for any string containing one recognized token. Clinical narrative almost always contains a date ("admitted 2026-01-05"), so a low-value token disables redaction of the high-value residual: the patient's name and diagnosis.
Proof (real phi_guard.scrub, strict=True):
PURE name+dx (no token) -> [redacted — free text on a PHI-bearing flow] (correct)
name+dx + a DATE -> "Patient John Michael Smith ... glioblastoma, admitted [date], poor prognosis." (NAME+DX LEAK)
auto-escalation {mrn, notes}, zero config:
notes -> "Patient John Michael Smith, glioblastoma, admitted [date], DNR discussed with family." (LEAK)
Where it lands. redact() output is what is DISPLAYED (the airlock approval card the human reviews) and STORED (the signed receipt — command_registry.make_receipt seals redact(inputs), and scrub_output scrubs the command OUTPUT into the receipt). It does not affect execution (payload_hash is over raw inputs), so receipts still verify; the defect is disclosure + informed-consent, not integrity. (I do not claim the redacted value reaches an external model call — redact output feeds the card and receipt, not the provider payload.)
Severity: MODERATE (proven: identifier-bearing clinical narrative is disclosed into the approval card and the stored signed receipt despite strict/auto-escalated redaction). Rises to HIGH only where that stored receipt or card crosses a confidentiality boundary in a given deployment (e.g. Teams receipt sync to other approvers) — stated as a condition, not assumed.
Distinct from the board. Not "compose surfaces bypass ALL redaction" (that was redaction not called; here it IS called and strict IS on). Not "v0.74 egress guard fails OPEN on its error path" (that was an exception path; this is the normal logic path, no error).
Fix (one line). In strict mode apply the wholesale free-text redaction regardless of token hits when the residual is still free text — reorder so strict and len>threshold wins, or redact wholesale first under strict. The recognized tokens are the low-value part; strict mode's whole purpose is the residual.
Reviewed adversarially against the source before posting.
Signed receipt (railcall verify → SIGNATURE VALID, offline):
{
"schema": "railcall_audit_receipt.v1",
"ran_at": "2026-08-13T13:17:25",
"file": {
"name": "findings_phi_strict.csv",
"sha256": "sha256:8c7931b7e39e32714a86b3d9c959c7d7f76cd9b2c61286bbb06fd5ffd1ad2590",
"bytes": 337
},
"audit": {
"rows": 1,
"columns": 4,
"import_breakers": 0,
"pii_columns": 0,
"formula_injection_cells": 0,
"findings": []
},
"network_audit": {
"lsof_available": false,
"error": "lsof_not_found",
"external_sockets_open": null
},
"result": "audited",
"receipt_version": "v2",
"flow": {
"dry_run": true,
"name": "audit",
"action_type": "audit"
},
"governance": {
"policy_ref": "none",
"policy_hash": "ff56072e81ed4908ea91f567741238b387e536cd1f5974513ee18df0d5c575b9",
"approval_chain": [],
"risk_classification": "unknown",
"irreversible": false
},
"execution": {
"input_sha256": "sha256:8c7931b7e39e32714a86b3d9c959c7d7f76cd9b2c61286bbb06fd5ffd1ad2590",
"output_sha256": "",
"duration_ms": 0,
"exit_code": 0
},
"signer_alg": "ed25519",
"public_key_hex": "ea2446fec9cc4de478c853fb35c778262d4327ac7d32d6ccff36bdbbfcd775e2",
"signature_hex": "122f1f6a245da0adf678218adb467cb729a45548b531fc2a1b19570de4817b2b0be49f2b82104f209b552ca94ab0d35821875455c02b7ca284892202b8906309"
}