Summary
The existing no-exec-interpolated-command rule only covers the @actions/exec global (exec.exec(...) / exec.getExecOutput(...)). Its concern is argument space-splitting, not shell injection. Node's child_process family — execSync, exec, spawn/spawnSync with { shell: true } — runs the command string through a shell, so an interpolated command string is a genuine shell-injection vector. No rule in eslint-factory currently covers this stricter, more severe class.
Evidence (grounded in the repo)
actions/setup/js/push_repo_memory.test.cjs:1155-1165 hand-rolls a regex assertion to forbid exactly execSync(\git ${variable}`)` patterns. This proves in-repo demand and confirms ESLint does not cover it today — the guard had to be re-implemented as an ad-hoc unit test.
- Real interpolated
execSync calls in production code: actions/setup/js/start_mcp_gateway.cjs:693, :824, :896 (command strings built with ${...} and passed to execSync).
Proposal
Add a dedicated rule no-child-process-interpolated-command (semantically distinct from no-exec-interpolated-command, which stays focused on the @actions/exec arg-splitting concern).
Acceptance criteria
- Flags an interpolated template literal or dynamic
+ concatenation passed as the command string to execSync, exec, spawn, spawnSync (and execFile/execFileSync when an options object with shell: true is present).
- Resolves the callee to a
child_process binding: require("child_process") / require("node:child_process"), ESM import, destructured { execSync }, and namespace member (cp.execSync). Reuse the existing binding-resolution helpers in src/rules/try-catch-rule-utils.ts (isRequireChildProcess, isChildProcessImportBinding, isChildProcessObjectBinding).
- Does NOT flag fully static command strings (literal / no-expression template / static
+).
- Does NOT double-report the
@actions/exec global — that remains owned by no-exec-interpolated-command.
- Diagnostic names the shell-injection risk and recommends passing arguments via an array (
execFileSync(cmd, [args])) or avoiding shell: true.
- Unit tests cover: bound
execSync, destructured { execSync }, namespace cp.execSync, static-safe command (valid), spawn(cmd, [args]) without shell (valid), and spawn(cmd, opts) / execSync with interpolation (flagged).
- Registered in
src/index.ts; documented in README.md with an "Out of scope" note distinguishing it from no-exec-interpolated-command.
Non-duplicate
Distinct from closed #47211 (execFileSync try/catch coverage) and #47212 (non-adjacent forward matching) — those concern error-handling, not command interpolation. no-exec-interpolated-command covers only the @actions/exec global.
Filed by ESLint Refiner (daily rule-quality refinement).
Generated by 🤖 ESLint Refiner · age00 317.3 AIC · ⌖ 13.1 AIC · ⊞ 4.6K · ◷
Summary
The existing
no-exec-interpolated-commandrule only covers the@actions/execglobal (exec.exec(...)/exec.getExecOutput(...)). Its concern is argument space-splitting, not shell injection. Node'schild_processfamily —execSync,exec,spawn/spawnSyncwith{ shell: true }— runs the command string through a shell, so an interpolated command string is a genuine shell-injection vector. No rule ineslint-factorycurrently covers this stricter, more severe class.Evidence (grounded in the repo)
actions/setup/js/push_repo_memory.test.cjs:1155-1165hand-rolls a regex assertion to forbid exactlyexecSync(\git ${variable}`)` patterns. This proves in-repo demand and confirms ESLint does not cover it today — the guard had to be re-implemented as an ad-hoc unit test.execSynccalls in production code:actions/setup/js/start_mcp_gateway.cjs:693,:824,:896(command strings built with${...}and passed toexecSync).Proposal
Add a dedicated rule
no-child-process-interpolated-command(semantically distinct fromno-exec-interpolated-command, which stays focused on the@actions/execarg-splitting concern).Acceptance criteria
+concatenation passed as the command string toexecSync,exec,spawn,spawnSync(andexecFile/execFileSyncwhen an options object withshell: trueis present).child_processbinding:require("child_process")/require("node:child_process"), ESM import, destructured{ execSync }, and namespace member (cp.execSync). Reuse the existing binding-resolution helpers insrc/rules/try-catch-rule-utils.ts(isRequireChildProcess,isChildProcessImportBinding,isChildProcessObjectBinding).+).@actions/execglobal — that remains owned byno-exec-interpolated-command.execFileSync(cmd, [args])) or avoidingshell: true.execSync, destructured{ execSync }, namespacecp.execSync, static-safe command (valid),spawn(cmd, [args])without shell (valid), andspawn(cmd, opts)/execSyncwith interpolation (flagged).src/index.ts; documented inREADME.mdwith an "Out of scope" note distinguishing it fromno-exec-interpolated-command.Non-duplicate
Distinct from closed #47211 (execFileSync try/catch coverage) and #47212 (non-adjacent forward matching) — those concern error-handling, not command interpolation.
no-exec-interpolated-commandcovers only the@actions/execglobal.Filed by ESLint Refiner (daily rule-quality refinement).