fix(hooks): block pipe-to-shell / eval RCE in denylist (#311) - #323
Merged
Conversation
segments() splits the command on the pipe separator so a downloader piped
into an interpreter (curl | sh, wget -O- | bash, echo | base64 -d | sh)
split into segments that individually matched no rule — remote code
execution was NOT blocked. There was also no eval-of-substitution rule.
Add a full-string rule path in check(): rules tagged scope:'full' test the
whole command, defeating the pipe split; all existing rules stay per-segment.
Two new full-string rules:
- pipe-to-shell: a downloader/decoder (curl|wget|fetch|base64) whose
pipeline feeds a shell interpreter (sh|bash|zsh|dash|ash|ksh), anchored
to the interpreter consuming the pipe so benign pipes (grep | wc -l,
curl | jq, cat | base64, tee notes.sh) still pass.
- eval-exec: eval of a command-substitution / backtick / decoded payload.
Return shape {blocked, rule, msg} and the fail-open guard are preserved. The
agy host inherits the fix via check(); emit test asserts agy parity. Regexes
use no nested quantifiers (no ReDoS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SATRHKa6mDHDuirhP6QuwL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #311
Problem
segments()inplugin/hooks/denylist.mjssplits the command on the pipe separator, so a downloader piped into an interpreter (curl | sh,wget -O- | bash,echo | base64 -d | sh) split into segments that individually matched no rule — remote code execution was not blocked. There was also no eval-of-substitution rule. The agy host inherits the gap viaplugin/hooks/agy-deny.mjs, which reusescheck().Fix
check(): rules taggedscope:'full'test the whole command (defeating the deliberate pipe split); all existing rules stay per-segment with unchanged behavior.pipe-to-shellrule: a downloader/decoder (curl|wget|fetch|base64) whose pipeline feeds a shell interpreter (sh|bash|zsh|dash|ash|ksh). The interpreter match is anchored to the command consuming the pipe, so benign pipes still pass.eval-execrule:evalof a command-substitution / backtick / base64-decoded payload.{blocked, rule, msg}and the fail-open guard are preserved. Fix flows to the agy host automatically viacheck().Acceptance criteria
tests/hooks/denylist.test.mjs"AC.1: curl/wget/fetch piped into sh/bash/zsh/dash is blocked" (7 payloads) +tests/agy/emit.test.mjsAC-289.2 spawns the emittedagy-deny.mjsand assertsdecision: deny/pipe-to-shell.curl | jq,cat | base64,tee run.sh).check()never throws and a hook error never blocks the session. Verified: "AC.4: garbage input never throws" + thehandle()journal-failure-still-blocks cases.Verification
pnpm verify: 555/555 passing (50 files).tests/hooks/denylist.test.mjs: 36/36..shfilenames and benign pipelines.🤖 Generated with Claude Code
https://claude.ai/code/session_01SATRHKa6mDHDuirhP6QuwL