Repository navigation
shell=True detection can be bypassed through variable assignment (malicious skill example that bypasses this static detection model) #475
Description
Activity
- changed the title
[-]shell=True detection can be bypassed through variable assignment[/-][+]shell=True detection can be bypassed through variable assignment (malicious skill example that bypasses this static detection model)[/+]on Sep 7, 2026 Reproduced on current
main(704bc954, SkillSpector 2.11.1) with the real static graph and--no-llmJSON CLI:- direct
shell=True: TM1 is present use_shell = True; shell=use_shell: TM1 is absent while AST4 remains- in the minimal full-scan fixture, this changes the result from score 45 /
CAUTIONto score 16 /SAFE
The root cause is that TM1 currently recognizes the direct spelling lexically, but does not resolve a statically definite name at the call site. I opened draft PR #497. Its first commit intentionally pins the failing runner-level regression; I am implementing the bounded AST augmentation and will push it after focused tests and independent local review passes.
The fix will keep unresolved or control-flow-ambiguous values on the existing AST4 path rather than guessing, and will preserve TM1-TM4's normalized/declared-marker/windowed lexical scans.
- direct
Implementation is complete and ready for review in #497 at exact head
005fff55874b45775e7f3fc789265145382caefe.The fix adds a bounded shared-AST companion to TM1 while preserving the existing lexical TM1-TM4 path. It now detects the issue's straight-line forms, including both
a = Trueand the reporter attachment'sa = 'True', when the name is passed asshell=ato a direct one-levelsubprocess.*or barePopencall. It reuses the existing TM1 contextual classification and avoids duplicate direct-literal findings.Verification on the final head:
- installed-wheel CLI: direct
True, boundTrue, and bound'True'each produced exactly one HIGH/0.9 TM1 with the same score (38); false and compound-boundary controls produced no TM1 - focused/shared/runner matrix: 95 passed; legacy reconstruction: 527 passed; public surfaces: 46 passed
- full repository suite: 4,018 passed, 14 skipped, 38 deselected, 4 xfailed
- source/wheel build and
twine check, Ruff, formatting, mypy, local Docker smoke, and real GitHub URL scan all passed - GitHub CI: changes, lint, unit, DCO, and Docker smoke all passed
- independent multi-pass review found no remaining issue within the supported contract; an adversarial 827 KB nested-control-flow input completed in 0.83 seconds
The analysis stays intentionally conservative beyond that contract: unsupported expressions, compound control flow, import aliases, arbitrary nested-call effects, and dynamic global mutation discard tracked facts rather than guessing. The PR uses
Fixes #475, so this issue will close when it merges.- installed-wheel CLI: direct
Superseding my earlier readiness comment for head
005fff55874b45775e7f3fc789265145382caefe: PR #497 is now ready for review at exact head89ccd3681cf72e06691ff9ec0e4c9eddcf3f0770.The final implementation detects the reported straight-line
a = True; shell=aform and the attacheda = 'True'variant for direct one-levelsubprocess.*and barePopencalls. It preserves the directshell=TrueTM1 severity, confidence, tags, fingerprint/risk behavior, source location, output caps, and report/ledger contracts. False and control-flow-ambiguous values do not gain TM1; unresolved calls retain the existing AST4 behavior.Exact-candidate verification:
- 889 affected/shared/public-surface tests passed
- full static suite: 4,457 passed, 14 skipped, 38 deselected, 4 xfailed (89% coverage)
- isolated built-wheel CLI matrix passed for direct/bound Boolean/bound string/false/control-flow cases across JSON, terminal, Markdown, and SARIF
- source/wheel build,
twine check, isolated install,pip check, Ruff, format, and mypy on all touched production modules passed - no-cache Docker build and smoke passed, including a real GitHub clone/scan; private reconciliation evidence did not leak
- fresh GitHub CI on the exact SHA passed: changes, lint, test-unit, DCO, and Docker smoke
- three independent defect-focused review passes found no remaining actionable issue within the documented supported contract
The analysis remains intentionally conservative for unsupported expressions, compound control flow, class/method or other dynamic effects, import aliases, and return/yield positions. Near-limit normalization-heavy inputs also have limited runtime headroom and continue to use the existing fail-closed partial/runtime-limit ledger contract.
PR: #497
TG follow-up superseding the readiness statement for exact head 89ccd36: independent installed-wheel and public-output testing found that executable extensionless Python (with a Python shebang) and .pyw files still preserve the #475 bypass. Direct shell=True is detected as HIGH TM1, while the equivalent flag=True; shell=flag form emits no TM1 and can remain SAFE with analysis_completeness=true. The cause is extension-only Python AST eligibility. I moved PR #497 back to draft. No source changes have been made for this finding yet; the proposed remediation is to use one bounded positive Python-source classifier for .py/.pyw and trusted Python shebangs across AST prewarm and static analysis, with public direct/bound parity and negative-classification tests before republishing readiness.
Superseding the extension-only blocker reported earlier: the remediation is now pushed to PR #497 at exact head
c986c3e4da2e7f6cee623ca2eac298a61a99a107.The fix now covers
.py,.pyw, trusted extensionless Python shebangs, nested/transitive artifacts, strict source decoding, and the relevant CPython/envexecution forms. Runtime-dependent source intent remains conservatively analyzed but is published as incomplete. Recursive failures and known risk also remain fail-closed under output caps, with parseable and secret-safe JSON/SARIF/Markdown output.Final local evidence includes:
- 5,223 repository tests passed with only the environment-blocked MCP stdio timing test excluded; 14 skipped, 39 deselected, 4 expected xfails
- 1,060 focused parser/TM1 tests passed independently on both Python 3.13 and 3.14
- fresh source and installed-wheel matrices passed 35/35 each; recursive source/wheel reporting passed 18/18
- live Darwin and GNU execution matrices passed 25/25 each; native/Docker PTY and bare
-c/-mexecution probes passed - real-root transitive cap 1/2 fault probes failed closed with exact typed diagnostics and no secret leakage
- sdist/wheel, isolated install, dependency/compile checks, no-cache arm64 Docker build/runtime, Ruff, formatting, and multiple independent review passes passed
One local environment gap remains explicit: endpoint-security and unrelated test I/O caused the fixed 15-second MCP stdio handshake test to time out; a simultaneous clean
origin/maincontrol timed out identically, while every other MCP server test and eight real installed-wheel MCP scans passed.Fresh GitHub CI has now completed on exact head
c986c3e4: changes, lint, test-unit, DCO, and Docker smoke all passed. The PR is open, not draft, mergeable, and reports a clean merge state.PR: #497
Updated implementation links: PR #497 was closed without merging after its work was split for focused review.
- Core bound
shell=truthiness fix: PR #577, open. - TM1 cross-window identity/reconciliation: PR #578, open.
- Additional Python execution surfaces: PR #579, open.
- The independent recursive scan/reporting split, PR #576, has merged; it does not itself fix the reported indirect-shell bypass.
The narrower alternative PR #560 also remains open. Keeping this issue open until the core fix is accepted and merged, with the cross-surface follow-ups tracked separately.
- Core bound
Resolution verification: PR #560 merged and automatically closed this issue. It detects the reported local literal assignment passed as
shell=, with regression coverage for reassignment and scope boundaries. Those tests pass on currentmainat844ac30.This supersedes my earlier in-progress comment: the narrower accepted implementation has landed. The additional work in #577, #578, and #579 remains separate follow-up scope. The merged fix is not yet in the latest published release, v2.11.2.
Follow-up merge update checked on 2026-10-08: PR #577 has now merged, extending the earlier accepted fix in PR #560.
The follow-up preserves lexical HIGH findings unless a retained bound-shell finding replaces them or a scoped replacement proof justifies abstention. Fresh offline verification on main
8f4ca7d6: 390 bound-shell, lexical-abstention, and cached-import regressions passed.This issue remains closed. PR #578 (cross-window identity) and PR #579 (additional execution surfaces) remain open as separate follow-up scope.
Summary
The scanner's
Tool Misusedetection can be bypassed by assigning theshellargument to a variable instead of usingshell=Truedirectly.Original:
Result:
Variant:
Result:
Expected Behavior
Both variants should receive equivalent findings because
astatically resolves toTrue.Impact
A simple variable assignment removes the
TM1high-severity finding and significantly reduces the overall risk score, which may affect installation recommendations.Suggested Fix
Add constant propagation / variable resolution for sensitive parameters such as
shell, so values that statically resolve toTrueare treated the same as directshell=True.Reproduction results
Here are the comparative JSON results provided
adv.json
ori.json