Skip to content

security(mitmproxy): use shlex.quote for sandcat.env generation - #101

Open
shejnowicz wants to merge 5 commits into
masterfrom
security/19-shlex-quote
Open

security(mitmproxy): use shlex.quote for sandcat.env generation#101
shejnowicz wants to merge 5 commits into
masterfrom
security/19-shlex-quote

Conversation

@shejnowicz

Copy link
Copy Markdown
Collaborator

Fixes #19.

Summary

Replaces the hand-rolled `_shell_escape` in `mitmproxy_addon_common.py` with stdlib `shlex.quote` when generating `sandcat.env` (the file `app-init.sh` sources to export env vars and secret placeholders into the agent container).

What this actually fixes (honest framing — this is robustness hardening, not an active-exploit fix; the old escaper did cover `\`, `"`, `$`, ` ``):

  • Newline corruption bug: the old code mapped a literal newline to `\n`, but inside shell double quotes `\n` is NOT an escape — a value containing a real newline was silently corrupted into a two-character backslash-n. A unit test even locked this wrong behavior in. shlex's single-quote strategy preserves newlines byte-for-byte.
  • `!` history-expansion edge: unhandled before; harmless in non-interactive sourcing but surprising when a user manually sources `sandcat.env` in an interactive bash.
  • Maintainability: deletes a hand-maintained escaping table in favor of the stdlib function that exists for exactly this.

Note: secret VALUES never land in `sandcat.env` — only `SANDCAT_PLACEHOLDER_*` strings do (built from regex-validated names, always shlex-safe). The variable-content path is the user-authored `env` block in settings.json.

Follow-up fix found in review

Preserving newlines made a latent consumer bug reachable: `app-init.sh`'s startup log counted and listed env vars by grepping `^export ` lines — a multi-line value's continuation line could phantom-match and print a value fragment into the log. Fixed by having the addon emit an authoritative names-only header as the first line of `sandcat.env` (`# names: A B C`, safe by construction — names are regex-validated), which `app-init.sh` now reads instead. The no-header fallback prints a nameless summary; it does NOT fall back to the value-leaking grep.

Format change

```

names: GIT_USER_NAME ANTHROPIC_API_KEY

export GIT_USER_NAME='Alice Example' # single-quoted when needed
export ANTHROPIC_API_KEY=SANDCAT_PLACEHOLDER_ANTHROPIC_API_KEY # bare when shlex-safe
```

Consumers audited: the only readers are shell `source` sites (`app-init.sh`, the `/etc/profile.d/sandcat-env.sh` copy, `su - vscode -c`) plus the now-fixed log summary. No other parser exists in scripts, bats, or compose.

Test plan

  • In-container pytest (mitmproxy 12.2.3 image, Python 3.14): 313 passed — includes new round-trip property tests (`shlex.split` on the emitted line recovers the ORIGINAL value — the real shell contract, not the escape spelling), hostile-input test (`$(rm -rf /)` + backtick) upgraded with round-trip assertion, newline-preservation test replacing the one that locked the old corruption, empty-string case, and a names-header test proving no value fragment can reach the header
  • Mutation-tested: the updated test suite run against the PRE-fix source fails on 10/12 targeted tests, including newline preservation and hostile input
  • Review verification: 13 hostile value classes (multi-line, trailing newline, CR, empty, unicode, `~`, `*`, `=`, `!`, embedded quotes, `$()`, backtick, backslash) delivered bit-perfect through every real consumer path: login bash via profile.d, double-source, dash + `set -e`, nested `su - vscode -c`
  • Hands-on integration on a real stack: hostile env value (`sp ace "dq" 'sq' $(reboot) `tick` $HOME ! end`) arrives in the agent container byte-for-byte — `$(reboot)` NOT executed, `$HOME` NOT expanded; placeholders still exported; `curl https://github.com\` → 200 through the proxy
  • Full bats surface green (14 suites)

🤖 Generated with Claude Code

https://claude.ai/code/session_01DFCiFbv1Cpr7yzCU8ftvzZ

shejnowicz and others added 5 commits August 19, 2026 10:27
Replace the hand-rolled _shell_escape with stdlib shlex.quote when
generating sandcat.env. Fixes silent newline corruption (\n is not an
escape inside shell double quotes), closes the interactive-shell `!`
history-expansion edge, and removes a hand-maintained escaping table.
Values flow from settings.json AND secret vaults, so robust quoting is
defense-in-depth. Two tasks: code+unit tests, hands-on container
verification of bit-perfect hostile-value delivery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFCiFbv1Cpr7yzCU8ftvzZ
 review)

shlex.quote preserves literal newlines in values, so a multi-line value's
continuation line could itself match app-init.sh's `^export ` grep,
producing a phantom var count and printing a fragment of the value to the
startup log. The addon now writes an authoritative `# names: ...` header
(built only from validated, whitespace-free names) as the first line of
sandcat.env, and app-init.sh parses that header instead of grepping export
lines. Falls back to a count-only message (no grep) if the header is
absent, for old-addon/new-init-script transitions.
…v format

Rename TestShellEscaping (and its test_*_escaped methods) to
TestEnvValueQuoting / test_*_preserved_via_quoting — shlex.quote quotes
values, it doesn't escape them, and the old names described behavior that
no longer exists. Add coverage for the new sandcat.env header (names-only,
no value fragments — including from a hostile multi-line value) and for
the empty-string value case. Update README's sandcat.env format example
from the old escaped-double-quote style to the shlex.quote reality.
Per repo convention, SDD spec/plan docs live in the working session, not
the PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use shlex.quote for shell escape

1 participant