Summary
no-child-process-interpolated-command and no-exec-interpolated-command share getDynamicCommandKind (eslint-factory/src/rules/command-initializer-utils.ts). For template literals, that helper unconditionally flags any interpolation:
if (candidate.type === AST_NODE_TYPES.TemplateLiteral && candidate.expressions.length > 0) return "interpolated template literal";
This fires the moment a template literal has any expression slot, without inspecting what the expression actually is. That's inconsistent with the same helper's handling of string concatenation / .replace() chains, which do resolve the underlying value via resolveWriteOnceInitializerChain before deciding whether it's genuinely dynamic.
Grounded false positives
actions/setup/js/start_mcp_gateway.cjs has three execSync calls (via const { spawn, execSync } = require("child_process"), line 33) that trip this exact pattern despite the interpolated values being defensively sanitized or path-derived immediately beforehand:
// lines 799-801
const safePort = String(gatewayPort).replace(/[^0-9]/g, ""); // digits only
execSync(`netstat -tlnp 2>/dev/null | grep ":${safePort}" || ss -tlnp 2>/dev/null | grep ":${safePort}" || echo "Port ${safePort} does not appear to be listening"`, { stdio: "inherit" });
// line 926
execSync(`node "${converterPath}"`, { stdio: "inherit", env: process.env }); // converterPath built via path.join(__dirname, ...)
// lines 996-998
const safePort = String(gatewayPort).replace(/[^0-9]/g, "");
execSync(`bash "${checkScript}" "${outputPath}" "(localhost/redacted) "$MCP_GATEWAY_API_KEY"`, { stdio: "inherit", ... });
All three interpolate values that are either regex-stripped to digits-only or derived from path.join(...) — the rule reports the same "interpolated command" warning it would give for genuinely untrusted input, with no way to distinguish the two. Since the rule runs at warn (not error), this kind of unaddressed noise makes it harder to spot real injection risk elsewhere.
Suggested fix
Extend the TemplateLiteral branch of getDynamicCommandKind to attempt the same resolution already used for concatenation:
- Resolve each interpolated expression via
resolveWriteOnceInitializerChain (or recurse into getDynamicCommandKind on it), and treat the literal as safe when every expression resolves to a static/literal value.
- Optionally recognize a narrow "known-safe sanitizer" shape (e.g.
String(x).replace(/[^0-9]/g, "") immediately assigned to the interpolated identifier) as non-dynamic.
Acceptance criteria
Generated by 🤖 ESLint Refiner · agent · 226.4 AIC · ⌖ 4.95 AIC · ⊞ 5.2K · ◷
Summary
no-child-process-interpolated-commandandno-exec-interpolated-commandsharegetDynamicCommandKind(eslint-factory/src/rules/command-initializer-utils.ts). For template literals, that helper unconditionally flags any interpolation:This fires the moment a template literal has any expression slot, without inspecting what the expression actually is. That's inconsistent with the same helper's handling of string concatenation /
.replace()chains, which do resolve the underlying value viaresolveWriteOnceInitializerChainbefore deciding whether it's genuinely dynamic.Grounded false positives
actions/setup/js/start_mcp_gateway.cjshas threeexecSynccalls (viaconst { spawn, execSync } = require("child_process"), line 33) that trip this exact pattern despite the interpolated values being defensively sanitized or path-derived immediately beforehand:All three interpolate values that are either regex-stripped to digits-only or derived from
path.join(...)— the rule reports the same "interpolated command" warning it would give for genuinely untrusted input, with no way to distinguish the two. Since the rule runs atwarn(noterror), this kind of unaddressed noise makes it harder to spot real injection risk elsewhere.Suggested fix
Extend the
TemplateLiteralbranch ofgetDynamicCommandKindto attempt the same resolution already used for concatenation:resolveWriteOnceInitializerChain(or recurse intogetDynamicCommandKindon it), and treat the literal as safe when every expression resolves to a static/literal value.String(x).replace(/[^0-9]/g, "")immediately assigned to the interpolated identifier) as non-dynamic.Acceptance criteria
actions/setup/js/start_mcp_gateway.cjs:801,:926,:998no longer report once the fix lands (or confirm any remaining report is a genuine risk that should be fixed at the call site, not the rule).