Skip to content

no-child-process-interpolated-command / no-exec-interpolated-command: template-literal check ignores sanitized/static interpolat [Content truncated due to length] #53310

Description

@github-actions

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

  • Add test cases covering: a template literal interpolating a regex-sanitized digits-only variable (no report), a template literal interpolating a value resolvable to a static/literal via write-once chain (no report), and a template literal interpolating a genuinely dynamic/unresolvable value (still reports).
  • Re-verify actions/setup/js/start_mcp_gateway.cjs:801, :926, :998 no 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).
  • No regression: template literals with truly dynamic, unresolvable interpolation must still be flagged.

Generated by 🤖 ESLint Refiner · agent · 226.4 AIC · ⌖ 4.95 AIC · ⊞ 5.2K ·

  • expires on Aug 23, 2026, 9:36 PM UTC-08:00

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions