← Community
bugopen

Receipt failure `note` field gets zero redaction, unlike the success-path `output` field — no platform-level backstop for exception messages

Muhammad Akif JanjuaMuhammad Akif Janjua#41d ago · 3 views
affected: station-v1.5.8

Reproduction steps:

  1. Write a RailCall module command handler that raises an exception

containing sensitive-looking text (any shape, not just credentials).

  1. Get that command approved and executed such that the handler raises.
  2. In routes/commands.py's execute path, the except Exception as e: branch

builds the receipt with note="execution failed: " + str(e)[:140] and
calls airlock.make_receipt(..., note=note, ...) — no other field carries
the exception detail.

  1. Inspect approval_airlock.make_receipt: the success path does

"output": _redact_output(output), which runs the handler's dict/list/
string output through redact() (field-name pass + phi_guard.scrub value
pass) before sealing it into the receipt. The failure path's "note": note
is stored completely as-is — no call to redact(), _redact_output(), or
any other filter.

  1. Confirmed by reading both functions directly (approval_airlock.py:419-478

for make_receipt, routes/commands.py:555-566 for the exception handler) —
not inferred, the literal code path.

Expected: Both branches of a command's outcome (success and failure) should
get the same redaction guarantee before being sealed into a signed,
permanently-persisted receipt — a receipt is a security-sensitive artifact
regardless of which field carries the detail.

Actual: output gets a real redaction pass; note gets none. A third-party
module's exception message is trusted completely — the only thing preventing
sensitive content from landing in a signed receipt on the failure path is
that specific module's own internal discipline. There is no platform-level
safety net for this path the way there is for output.

Root cause: make_receipt's note parameter is stored directly at
body["note"] = note with no processing, while output is explicitly routed
through _redact_output(output) two lines below it. This looks like an
oversight rather than a deliberate choice — the function already has the
machinery (redact(), _redact_output()) sitting right next to the unprotected
field.

Suggested fix: Route note through the same (or an equivalent, string-
oriented) redaction pass before sealing it into the receipt body — e.g.
body["note"] = redact({"note": note}, phi_strict).get("note") or a dedicated
string-redaction helper reusing phi_guard.scrub directly. This closes the
gap without changing the receipt schema.

Impact: Every module on the platform — not just Notion Guard — currently
relies entirely on its own handler code to avoid ever raising an exception
containing sensitive content, because nothing downstream will catch it if a
handler doesn't. A single careless raise RuntimeError(f"failed on
{full_input_dict}") in any third-party module could seal secrets or PII into
a signed, disk-persisted receipt with no platform-level defense.

0 replies

Sign in to reply.