Skip to content

Enumerate a Value's Untrusted Surfaces, Not Its Fields — and Guard by Construction

Problem

claude-boot adopt prints a block that the workflow tells an agent to relay verbatim and the owner to run unchanged. Untrusted bytes reaching that block are therefore one copy-paste away from a shell.

Four review passes each found a NEW injection site in the same module, each on a field the previous fix had not covered:

Pass Field Surface
1 cliSessionId a JSON field value
1 title, question headers JSON field values
3 the entry's file name the basename on disk
4 account / org the directory names

Every individual fix was correct. The process was still wrong, because each one answered "is this field safe?" when the question that terminates is "what are the untrusted surfaces?"

Root cause

Two compounding mistakes.

1. The object was never surface-enumerated. A DesktopEntry is read from <store>/<account>/<org>/local_<id>.json. That path yields three independent attacker-authored surfaces:

  1. the JSON field values,
  2. the file basename,
  3. the two directory names the record's account and org are derived from.

Passes 1–3 hardened surfaces 1 and 2, one field at a time. Surface 3 was never listed, so nothing in the process could have found it except another bug report.

2. A shell guard was mistaken for a display guard. The command line used shlex.quote(account). That is correct for the shell: it wraps the value so the shell reads one word. It does not flatten anything — newlines and ESC[2K survive byte-for-byte inside the quotes, and the terminal renders them. A hostile directory name therefore printed a standalone runnable line inside a genuine command.

The guard-to-case map recorded the error in writing: "shlex.quote on the command line — NONE, unreachable while the shape check holds." True for the ids on that line. False for --account/--org on the same line.

Resolution

Enumerate surfaces, then guard by construction.

  1. List the surfaces of the value, not the fields of the struct. "Where do the bytes of this object come from?" is answerable and finite; "is field X safe?" is neither.
  2. Make the guard structural. Every output line now goes through one formatter that flattens each interpolated argument. A new print cannot forget the guard because there is nothing to remember. Mutation evidence: disabling the flattening turns seven assertions red across three cases, where per-field guards turned one or two.
  3. Refuse rather than mangle where the output must stay executable. Flattening a command produces a command that differs from the one that works. For a target that cannot be named safely, print no command and fall through to an already-sanitized list.
  4. Layer deliberately, and prove each layer separately. Where two locks exist, find an input each catches alone — otherwise both survive mutation individually and the "two locks" claim in the docstring is unbacked. That happened here and was only caught by mutating each half.

Prevention

  • When a review finds an injection site, do not fix the field. Enumerate the surfaces first and ask which others share the provenance. If the answer is "I have not listed them", the next pass will find the one you missed.
  • Treat "N passes each found one more instance" as a signal about the method, not the code. A fourth patch is evidence the approach is wrong.
  • Name what a guard actually guarantees. shlex.quote is shell quoting, _safe() is display flattening, a shape check is admission control. They are not interchangeable, and a note claiming one covers the other is a bug with a long fuse.
  • Prefer one chokepoint over N disciplined call sites. Coverage that depends on memory decays at exactly the rate new call sites are added.
  • docs/solutions/patterns/security-fixtures-must-prove-reachability.md — the testing half of the same review, and the reason these findings were trustworthy at all.
  • SPEC-203 intake/pending/spec-203-followup-adopt-write-path-hardening.md — the surviving lower-severity items, and the sweep that produced the surface enumeration.