fix(security): inline shell variable indirection before dangerous-com… - #61434
fix(security): inline shell variable indirection before dangerous-com…#61434MorAlekss wants to merge 3 commits into
Conversation
Related to the approval-gate hardening family: sibling open PR #57666 (interpreter-name/flag obfuscation in the same |
…ndirection coverage
|
Follow-up commit: resolves empty-value assignments ( |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating a real approval-gate bypass class. Current main still returns from _normalize_command_for_detection() immediately after $IFS handling (tools/approval.py:852-853), so simple variable indirection is not already covered.
Problems
tools/approval.py:883-923builds one command-wide assignment map and replaces all references globally. That changes shell semantics for scoped command-prefix assignments andunset; for example,unset H; H=~/.bashrc env true; sed -i s/a/b/ $Hhas noHafterenv true, but this pass rewrites the final$Has if it did.- The assignment regex at
tools/approval.py:883-887cannot see an assignment immediately after$(,(, or{. Existing parsing recognizes those command starts later (tools/approval.py:1311-1324), soecho $(H=~/.bashrc; sed -i s/a/b/ $H)remains unresolved. tests/tools/test_approval.py:1355-1358expects a quoted~assignment to behave as a home-path assignment; quoted tilde is literal in shell assignment syntax.
Suggested changes
- Make substitution scope- and position-aware using the existing quote-aware shell tokenizer, including grouped command contexts.
- Add regressions for command-prefix scope,
unset, and$()/ subshell / brace-group forms; remove the quoted-tilde expectation.
GitHub reports the branch MERGEABLE/CLEAN, so the corrected work remains salvageable. Automated hermes-sweeper review.
|
Thanks @teknium1, all three addressed and verified against your exact examples:
Added regressions for all of the above, including |
|
suggesting changes The variable-indirection handling is incomplete: shell assignments in control contexts and unresolved variable composition can still bypass approval checks, allowing sensitive edits or destructive operations to run without the intended gate.
Security evidence:
Not checked:
Signed: GPT-5.6-luna-max in Codex |
fix(security): inline shell variable indirection before dangerous-command detection Excellent, carefully-scoped security fix with outstanding test coverage (scope-aware assignment semantics, chained/circular refs, separator contexts, negative controls). A few residual edges worth considering:
|
Summary
Fixes a gap where shell variable indirection lets the exact same
sensitive-path and hardline command shapes this file's patterns exist
to catch slip past detection entirely.
sed -i s/a/b/ ~/.bashrciscorrectly flagged;
H=~/.bashrc; sed -i s/a/b/ $Hexecutes identicallyin any POSIX shell but was not flagged at all, since the patterns match
literal text and the unresolved
$Hnever appears in_SENSITIVE_WRITE_TARGET.This isn't a narrow, one-off gap. The same unresolved-variable blind
spot applies uniformly across the sensitive-write-target layer:
sed -ion
~/.bashrc/~/.hermes/config.yaml/.env,tee/redirect writes to~/.ssh/authorized_keysand similar paths, and even the command nameitself (
a=rm; $a -rf /bypasses the hardline floor the same way). Onetechnique devalues the whole layer, not a single pattern.
It's also a lower-effort trigger than most of the interpreter-flag gaps
found elsewhere in this file recently. Those needed a specifically
constructed, unusual flag combination that requires deliberate intent
to produce.
H=~/.bashrc; sed -i ...is an ordinary, everyday shellidiom: assign a path to a variable, use it later. A request as mundane
as "add an alias to my bashrc" can plausibly produce a script shaped
exactly like the bypass, with no adversarial intent involved at all.
Root cause
_normalize_command_for_detection()already de-obfuscates severalshell mechanisms before pattern matching (backslash escapes, empty-
string literals,
$IFS/${IFS}word-splitting, absolute home-pathprefixes) precisely because the patterns below it match literal text
and would otherwise miss the same command shape spelled differently.
Shell variable assignment/expansion (
VAR=value; ...$VAR...) was notamong them, despite being handled by the exact same class of reasoning
already documented for the other cases in this function.
Behavioral change
Before:
H=~/.bashrc; sed -i s/a/b/ $H(and the equivalent fortee,redirects, and the command name itself) passed through undetected,
identical in shell semantics to a literal form that was correctly
caught.
After:
_inline_simple_var_assignments()inlines simpleNAME=valueassignments (including
export/local/declare-prefixed forms) intolater
$NAME/${NAME}references within the same command, run inside_normalize_command_for_detection()alongside the existing$IFShandling. Simple variable-to-variable chains (
H2=$Hreferencing anearlier
H=...) resolve through a bounded number of passes, and acircular reference terminates safely without hanging. Value-side
command substitution (
H=$(echo ...)) does not resolve — see "What isNOT changed" below.
Because
_normalize_command_for_detection()sits inside the shared_command_detection_variants()pipeline, this fix applies uniformly todetect_dangerous_command(),detect_hardline_command(), and_match_user_deny_rule()(the user-editableapprovals.denylist) —not just one of the three.
What changed
tools/approval.py:_inline_simple_var_assignments(), called from_normalize_command_for_detection()right after the$IFScollapseexport/local/declare(with flags) prefixes, and multi-level variable chainsIFSfrom generic inlining (handledseparately, immediately above) and leaves values containing command
substitution (
$(...)) unresolved rather than guessing at theircontent
tests/tools/test_approval.py:TestVariableIndirectionBypassclass (21 tests) covering thesed -i/tee/redirect/command-name bypass shapes, chained andexport/local/declare-prefixed assignments, a circular-referencesafety check, benign variable usage that must stay unflagged,
$HOMEfrom the real environment correctly left untouched, and the
command-substitution limitation pinned as an explicit, accepted
non-goal rather than a silent gap
What is NOT changed
H=$(echo ...)) isnot resolved. Statically resolving arbitrary command substitution
would require actually executing code, which this detector will
never do; leaving it unmatched is the safe failure mode; it does not
widen any existing bypass, it simply doesn't help with this specific
case. Simple variable-to-variable chains resolve; value-side command
substitution does not
is unrelated to this fix; verified no merge conflicts between the two
new tests above)