Skip to content

Scope child_process_exec to JS/TS files (fix Python false-positive)#57888

Open
emora-hash wants to merge 1 commit into
anthropics:mainfrom
emora-hash:fix/security-guidance-child-process-exec-scope
Open

Scope child_process_exec to JS/TS files (fix Python false-positive)#57888
emora-hash wants to merge 1 commit into
anthropics:mainfrom
emora-hash:fix/security-guidance-child-process-exec-scope

Conversation

@emora-hash
Copy link
Copy Markdown

Scope child_process_exec to JS/TS files (fix Python false-positive)

Summary

The child_process_exec rule in security_reminder_hook.py uses the substring "exec(" to match child_process.exec() invocations. This substring also matches Python's asyncio.create_subprocess_exec(, which is the safe Python variant (uses posix_spawn, no shell, no command-injection risk).

The hook fires on first Write of any Python file containing create_subprocess_exec( and emits a JavaScript-flavored warning recommending execFileNoThrow.ts — confusing in a Python context and pure noise.

This PR adds an optional file_extensions field to SECURITY_PATTERNS entries and applies it to child_process_exec so the rule only fires on JS/TS files where child_process.exec() is actually a concept.

Repro (before fix)

# file: services/runner.py
import asyncio
async def spawn():
    proc = await asyncio.create_subprocess_exec(
        "/usr/bin/ls", "-la",
    )

First Write triggers the child_process_exec warning. Second Write is suppressed (per-session state). Cosmetic friction + misleading recommendation.

Change

  • New optional field on pattern entries: file_extensions: list[str] (lowercase extensions including leading dot).
  • New helper _path_extension(normalized_path) returns the lowercase extension or empty string.
  • check_patterns() skips a substring rule when its file_extensions is set and the current file's extension isn't included.
  • Rules without file_extensions keep the legacy behavior (apply to all files) — fully backward-compatible.
  • Applied to child_process_exec only in this PR: [".js", ".ts", ".tsx", ".jsx", ".cjs", ".mjs"].

Other potentially language-specific rules (new_function_injection, react_dangerously_set_html, document_write_xss, innerHTML_xss, pickle_deserialization) are intentionally untouched in this PR to keep scope minimal. Follow-up PRs can scope each individually after this mechanism lands.

eval_injection is deliberately left universal — the eval builtin is dangerous in JS, Python, Ruby, and beyond.

Test

Included test_fix.py (smoke test, not part of the PR — just included here for the reviewer's confidence). 3 cases:

Case Expected Pre-fix Post-fix
Python file w/ asyncio.create_subprocess_exec( no match false-positive skipped
JS file w/ child_process.exec( match child_process_exec matches matches
Python file w/ eval( match eval_injection (universal) matches matches

All 3 pass.

Files changed

  • plugins/security-guidance/hooks/security_reminder_hook.py (+20 / -0)

Notes for reviewer

  • The fix is additive — no changes to existing rule definitions or behavior.
  • _path_extension() deliberately handles the "no extension" and "dot-in-dirname-only" cases (e.g., /path/with.dot/no-extension returns "").
  • Filter uses .lower() so extensions in YAML/manifest comparison and case-mismatch in user paths both work.
  • For maximum portability with the existing pattern data structure, the field is optional: any rule that doesn't declare file_extensions keeps universal matching.

Adds optional file_extensions filter to SECURITY_PATTERNS. Scopes the
child_process_exec rule to .js/.ts/.tsx/.jsx/.cjs/.mjs so it no longer
false-positives on Python's asyncio.create_subprocess_exec(.

Other rules keep universal behavior (no file_extensions field set).
Backward-compatible.
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.

1 participant