Skip to main content
interlace
Plugin: mcp-sdk-securityRules

no-code-injection-in-tool

Disallow an MCP tool argument reaching eval, new Function or a vm context as code.

CWE: CWE-95 OWASP: A03:2021 – Injection

Detects a tool argument evaluated as JavaScript by eval, Function, a string setTimeout / setInterval, node:vm or a vm2 sandbox. This rule is part of eslint-plugin-mcp-sdk-security.

💼 This rule is set to error in the recommended and strict configs.

Quick Summary

AspectDetails
CWE ReferenceCWE-95 (Eval Injection)
SeverityCritical (CVSS 9.8)
Auto-Fix❌ No auto-fix available
CategorySecurity

Why this matters

"Calculator" and "run a snippet" tools evaluate what the model sends them. The model can be steered by anything it has read, so the expression it sends is attacker-chosen, and eval runs it with the server's full privileges: require('child_process').execSync(...) is a valid arithmetic expression as far as eval is concerned. node:vm is not a security boundary, and vm2 was discontinued after repeated sandbox escapes.

Relationship to node-security/detect-eval-with-expression

node-security/detect-eval-with-expression and browser-security/no-eval report a non-literal eval anywhere. Inside an MCP tool handler the source is the handler's own parameter, so this rule follows it — through bindings, derivations and same-file helpers — and also covers vm and vm2. Both may report one line; tool handlers are this plugin's scope (owner decision, 2026-10).

❌ Incorrect

// ❌ a calculator that evaluates JavaScript
server.registerTool('calc', cfg, async ({ expr }) => ({
  content: [{ type: 'text', text: String(eval(expr)) }],
}));

// ❌ every argument of Function is code
server.registerTool('fn', cfg, async ({ body }) => new Function('x', body));

// ❌ node:vm is not a sandbox
server.registerTool('run', cfg, async ({ code }) =>
  vm.runInNewContext(code, {}),
);

How sinks and arguments are recognised

  • Globals — eval, Function (called or constructed; every argument is code), and a string passed to setTimeout / setInterval — count only when no local binding shadows them.
  • node:vm — runInNewContext, runInThisContext, runInContext, compileFunction, new vm.Script(...) and new vm.SourceTextModule(...) — is resolved to the module, through imports, namespaces and require().
  • vm2 — run(...) on an instance built by its VM or NodeVM class.
  • Arguments are tool arguments, request.params.arguments and ResourceTemplate variables, followed through bindings, templates, concatenation, string derivations and same-file helpers.
  • Into helpers, elements and parsed arguments (1.0). A same-file function or class method (this.run(args.cmd)) the handler calls carries what its callers pass it, so a sink inside the helper is reported — an object literal it is given is read field by field. Each element of an argument array is the argument (for (const p of paths), paths.map((p) => …)), and a Zod Schema.parse(args) / safeParse(args).data hands the arguments back: parsing checks their shape, not their text. A key the parsing schema restricts to z.enum / z.literal / z.nativeEnum stays the allowlist. Callers are found by resolving the callee, never by a name.

✅ Correct

import { evaluate } from 'mathjs';

// ✅ a grammar that only accepts arithmetic
server.registerTool('calc', cfg, async ({ expr }) => ({
  content: [{ type: 'text', text: String(evaluate(expr)) }],
}));

// ✅ data is parsed, not evaluated
server.registerTool('load', cfg, async ({ json }) => JSON.parse(json));

What this rule deliberately does not report

  • A literal or a value that is not a tool argument.
  • setTimeout with a function, which evaluates nothing.
  • A library evaluator (mathjs' evaluate, a template engine): whether it is safe depends on its configuration, which this rule cannot see.
  • A file that imports no MCP server package.

When Not To Use It

A tool whose purpose is executing model-written code must run it in a separate process or isolate with no host access. Disable this rule for that file with a comment naming the isolation.

Further Reading

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.