Daemon reason/error strings cross the trust boundary into agent transcripts that leave
the machine. To keep RPC API keys out of them, sanitize_reason
(crates/deckard-signerd/src/config.rs) scrubs every whitespace-delimited token
containing :// down to scheme://host[:port] before the reason is truncated and
returned. The T9 transcript allowlist scan is the backstop.
The claim to attack
- For realistic transport-error shapes (alloy/reqwest echoing the full request URL, with
the key in the path/query/userinfo), the URL-token scrub catches the credential — this
is unit-tested and canary-tested end to end.
Where the code is
crates/deckard-signerd/src/config.rs — sanitize_reason (token-based URL scrub),
redact_url.
crates/deckard-signerd/src/daemon.rs — one_line (the wrapper that runs
sanitize_reason then truncates to 160 chars on every broadcast_failed / signer-error
reason), and the broadcast_failed: {…} / signer_error: {…} / railgun_keys: {…} call
sites that feed it arbitrary upstream error text.
What a successful finding looks like
- A realistic transport/RPC/library error whose text carries the credential in a shape
with no :// in the same whitespace token — so the scrub leaves it untouched — and
that still gets surfaced in a reason. Examples worth probing: a bare
Authorization: Bearer <token> header echoed in an error, a key in a JSON error body,
a host=…&apikey=…-style query fragment split onto its own line, an error that puts the
URL after a newline that one_line discards but with the key duplicated elsewhere, or a
proxy/DNS error format that prints user:pass@host without the scheme.
- Show the leak reaches a tool transcript (i.e. it also slips the T9 allowlist scan, or
argue why the scan wouldn't catch that shape).
Severity framing
High if reproducible against a real RPC provider's error text — a leaked API key is a
funds-adjacent privacy/cost compromise. The fix is likely an allowlist-of-shapes or an
entropy/secret-pattern scrub in addition to the URL-token rule; a good finding tells us
which shape to add.
Daemon
reason/error strings cross the trust boundary into agent transcripts that leavethe machine. To keep RPC API keys out of them,
sanitize_reason(
crates/deckard-signerd/src/config.rs) scrubs every whitespace-delimited tokencontaining
://down toscheme://host[:port]before the reason is truncated andreturned. The T9 transcript allowlist scan is the backstop.
The claim to attack
the key in the path/query/userinfo), the URL-token scrub catches the credential — this
is unit-tested and canary-tested end to end.
Where the code is
crates/deckard-signerd/src/config.rs—sanitize_reason(token-based URL scrub),redact_url.crates/deckard-signerd/src/daemon.rs—one_line(the wrapper that runssanitize_reasonthen truncates to 160 chars on everybroadcast_failed/ signer-errorreason), and the
broadcast_failed: {…}/signer_error: {…}/railgun_keys: {…}callsites that feed it arbitrary upstream error text.
What a successful finding looks like
with no
://in the same whitespace token — so the scrub leaves it untouched — andthat still gets surfaced in a
reason. Examples worth probing: a bareAuthorization: Bearer <token>header echoed in an error, a key in a JSON error body,a
host=…&apikey=…-style query fragment split onto its own line, an error that puts theURL after a newline that
one_linediscards but with the key duplicated elsewhere, or aproxy/DNS error format that prints
user:pass@hostwithout the scheme.argue why the scan wouldn't catch that shape).
Severity framing
High if reproducible against a real RPC provider's error text — a leaked API key is a
funds-adjacent privacy/cost compromise. The fix is likely an allowlist-of-shapes or an
entropy/secret-pattern scrub in addition to the URL-token rule; a good finding tells us
which shape to add.