Safeguard
Security

eslint-plugin-security: How to Catch Node.js Security Bugs at Lint Time

eslint-plugin-security adds static-analysis rules to ESLint that flag risky Node.js patterns before they ship. Here is how to configure it and read its warnings without drowning in noise.

Priya Mehta
DevSecOps Engineer
5 min read

eslint-plugin-security is an ESLint plugin that flags potentially dangerous Node.js patterns — things like child_process with a dynamic argument, eval, unsafe regular expressions, and non-literal filesystem paths — while you are still editing code. It will not prove your app is safe, but it moves a whole class of mistakes from a runtime incident to a red squiggle in your editor.

The plugin lives under the eslint-community organization on GitHub, and the current major line is 4.x, published to npm as eslint-plugin-security. It ships a recommended config and roughly a dozen rules, all heuristic. That word matters: these rules pattern-match on the shape of your code, not on a real data-flow analysis, so they produce both misses and false positives. Used with that expectation, they are worth the five minutes it takes to add them.

What the rules actually detect

Each rule targets one recognizable footgun. A few you will hit most often:

  • detect-child-process warns when you call child_process.exec with anything other than a string literal, because a concatenated command is a shell-injection candidate.
  • detect-non-literal-fs-filename fires when a path passed to fs comes from a variable, which is the tell for path traversal.
  • detect-non-literal-regexp and detect-unsafe-regex catch regular expressions that could be built from user input or that contain catastrophic-backtracking patterns leading to ReDoS.
  • detect-eval-with-expression flags eval on a non-constant string.
  • detect-object-injection warns on bracket access like obj[userKey], which can reach __proto__ and enable prototype pollution.

That last one, detect-object-injection, is famous for noise. obj[i] inside a for loop is completely safe and the rule still complains. Most teams disable it globally and re-enable it only in modules that handle untrusted input.

Installing and configuring it

Add the plugin as a dev dependency:

npm install --save-dev eslint eslint-plugin-security

ESLint 9 uses flat config in eslint.config.js. The plugin exposes a ready-made recommended config you can spread in:

import security from "eslint-plugin-security";

export default [
  security.configs.recommended,
  {
    rules: {
      // opt out of the noisiest rule, keep the rest
      "security/detect-object-injection": "off",
    },
  },
];

If you are still on the legacy .eslintrc format, the equivalent is:

{
  "extends": ["plugin:security/recommended"],
  "rules": {
    "security/detect-object-injection": "off"
  }
}

Run it the same way you run any lint pass — npx eslint . — and wire it into your pre-commit hook and CI so a warning cannot merge silently.

Reading a warning without panicking

When the plugin reports detect-child-process, it is not saying "this is a vulnerability." It is saying "a human should confirm this input is trusted." Treat every hit as a question, not a verdict. Walk the value back to its source. If it originates from a config constant, suppress the line with an inline comment and a reason:

// eslint-disable-next-line security/detect-child-process -- cmd is a hardcoded allowlist entry
exec(cmd);

Write the reason. A bare eslint-disable with no explanation is how a genuine bug hides behind a suppression six months later.

Where it fits, and where it doesn't

Linting sees one file at a time. It cannot follow tainted data from an HTTP handler, through three service functions, into a database call — so it will not find most real injection chains. That is the job of dedicated SAST and of your test suite. eslint-plugin-security is the cheap first layer: it kills the obvious stuff so reviewers spend their attention on logic.

It also says nothing about your dependencies. A clean lint run on code that imports a package with a known CVE is still exposed. Composition analysis is a separate discipline; a tool such as Safeguard can flag a vulnerable transitive dependency that no linter will ever see. If you want to understand how those two layers differ, our SCA product overview breaks down what dependency scanning covers that source linting cannot, and the Academy has a broader walkthrough of where each control belongs in a pipeline.

Tuning it for a real team

The failure mode with any security linter is alert fatigue: turn everything to error, watch developers reflexively disable the plugin. Avoid that by starting the recommended set at warn, promoting individual rules to error only once you have cleared the existing hits, and disabling detect-object-injection unless you have a module that genuinely handles arbitrary keys. Review the suppression comments in code review the same way you review the code — an unexplained disable is a smell.

Pin the version in your lockfile and update deliberately. New minor releases occasionally tighten a heuristic, which can surface a batch of new warnings; you want that to land in a controlled PR, not mid-release.

FAQ

Does eslint-plugin-security replace a SAST scanner?

No. It is a lightweight single-file heuristic linter. A real SAST tool performs data-flow and taint analysis across your whole codebase and finds injection chains that a linter cannot see. Run both; they overlap far less than the names suggest.

Why does detect-object-injection fire on obviously safe code?

The rule flags any computed member access with a non-literal key, including array indexing in loops. It cannot tell a loop counter from attacker-controlled input, so it over-reports. Most teams disable it globally and enable it only where untrusted keys are handled.

Which version should I use?

Install the latest published eslint-plugin-security from npm — the current major line is 4.x, maintained under the eslint-community org. Match it to an ESLint version it supports and pin it in your lockfile so heuristic changes arrive in reviewable updates.

Can I run it only on changed files?

Yes. Point ESLint at the files in your diff in CI, or let a pre-commit hook lint staged files. That keeps feedback fast on large repositories while still gating merges on the full-tree run.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.