# eslint-plugin-security enables fifteen rules and warns you about them in sentence two

> eslint-plugin-security is a set of fifteen static analysis rules for Node security hazards, published as a CommonJS package with bundled type definitions and a single runtime dependency. It warns in its second sentence that it produces many false positives needing human triage, one rule flags both the loose and the strict equality operators, and three different changelog and release mechanisms share the repository.

**eslint-community/eslint-plugin-security** — ESLint rules for Node Security

- Repository: https://github.com/eslint-community/eslint-plugin-security
- Stars: 2,379 · Forks: 114
- Language: JavaScript
- License: Apache-2.0
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/eslint-community-eslint-plugin-security

## The readme warns about false positives before it explains installation

The second sentence of the readme is the one that matters: the project says it will help identify potential security hotspots, but that it finds a lot of false positives which need triage by a human.

That placement is unusual and it is the correct one. A linter that reports everything at error level and says nothing about its noise floor is a plugin teams disable in a week. This one tells you the cost before you have installed anything, and the install is one line:

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

The corpus behind the warning is fifteen rules, and all fifteen are set in the recommended configuration. The rules table carries a legend for configurations set to warning level and a marker for rules in the recommended set, and every one of the fifteen carries that marker. So there is no quiet rule and no opt-in-only rule: taking the recommended preset means taking all of them.

The rules fall into recognisable families. Some are Node-specific, covering a deprecated buffer construction form, a non-literal filename passed to the filesystem module, child process execution with a non-literal argument, and pseudo-random byte generation where real randomness was probably intended. Some are general JavaScript hazards: evaluating a variable, requiring a variable, and property access through a variable key, which is the shape of a prototype-pollution bug. Two concern source encoding rather than runtime, detecting bidirectional and invisible Unicode characters that can hide code in a diff. Two are framework-specific, covering a template engine's escape flag being turned off and a middleware ordering mistake in one server framework.

And two concern regular expressions, one for statically written unsafe patterns and one for patterns built from a variable.

## One rule flags the strict equality operators as a timing risk

The rule for timing attacks is the one to read closely, because its own description names four operators: two loose comparisons and two strict ones.

Loose equality performs a type coercion before comparing, which is a correctness hazard and can leak information through timing in the way the rule describes. Strict equality does not coerce, and comparing with it is not the timing-risk case the rule is named for.

So the rule fires on the safe comparison as well as the unsafe one. That is not a bug in the rule so much as a decision about where to draw the line: catching the loose form inside a codebase that uses strict equality everywhere is noise, and the pragmatic answer is to flag both and let the author say which.

It is also a fair sample of the rest of the corpus. Several rules fire on patterns that are dangerous only in combination with something else, or dangerous only in a context the rule cannot see. Non-literal property access is the prototype-pollution shape, but almost all uses of computed access are a lookup table. A non-literal require is exactly the dynamic-load hazard, and also exactly every plugin system and every configuration-driven module.

Which is why the readme's warning and the fifteen always-on rules are the same fact stated twice: the value is in triaging, and a team that treats the output as a gate will spend its budget on the coercion case and the lookup tables.

## Two config paths, different preset names, and a CommonJS flat-config example

The package is CommonJS and the two configuration paths are shaped differently.

The current path is flat config, and it requires a linter at 8.23 or newer. The example loads the plugin with a require call and exports the plugin's recommended configuration as an array entry in a module exports, which is the flat-config array shape. Note that the example itself is CommonJS: a project whose config file is an ES module would need an import rather than a require, and the readme does not show that variant.

The deprecated path is the older format, and its preset has a different name. It does not say extend the recommended preset; it says extend a preset with a legacy suffix. So a project migrating from the old format to the new one does not get the same preset under a new spelling, it gets a differently named one, and a project that has copied the string into its configuration will find the name it holds is not the name the new format wants.

That is a small thing and it is the kind of small thing that appears in an upgrade note rather than in the main documentation. The recommendation in the developer section is the practical answer: implement a test for each new rule, and run the one command that runs the tests and the lint together before opening a pull request.

## Types are bundled, and the floor is a linter release or an extra type package

Most plugins of this kind do not ship their own types and tell you to install a stub from a community type repository. This one bundles them.

The readme says the type definitions ship with the package so nothing else needs installing, and then gives the requirement that replaces it: they are written against the linter's version 9 definitions. So you need either a linter at 9.10 or newer, which the readme identifies as the first release shipping its own definitions, or the separate v9 type package installed alongside an older linter.

The migration note follows. If you previously installed the stub type package for this plugin from the community type repository, it is no longer needed, and both package-manager forms of the removal command are given.

That is a well-handled transition and worth noting as one. A plugin that stopped needing a community-maintained stub has to handle three audiences at once: people on the current linter who need nothing, people on an older linter who need an extra package, and people who installed the stub and now have a conflicting duplicate. All three are covered, and the removal command is copy-pasteable in either package manager.

The one thing absent is a statement of what happens if you are on a linter older than 9.10 and do not install the extra package. Presumably the types fail to resolve, and that is a reasonable outcome, but it is not written down.

## Type linting runs a package-shape checker alongside the compiler

The type lint step is two commands joined by an operator, and the second one is the interesting one.

The first compiles with no output and no emit, which is the ordinary type check. The second runs a package-shape checker in packing mode, so it builds the package as it would be published and inspects the result.

That second check exists because bundling types is easy to get wrong in ways the compiler cannot see. A package can type check perfectly and still resolve types incorrectly for consumers, depending on how the module is declared, where the definitions live relative to the entry point, and whether the exports map covers the conditions a bundler or a runtime will look under. Those are packaging facts rather than type errors, so no amount of compiling finds them.

The package in question is CommonJS with a single types entry, which is exactly the combination where consumers on an ES module runtime can end up with types that do not resolve. Running the shape checker on the packed output is the direct way to catch it, and it means the repository is checking the artefact it publishes rather than the source it wrote.

It is also the one lint step that validates something outside the repository entirely, since the thing being checked is what a consumer will receive.

## Three changelog mechanisms, and the tag format belongs to another tool

The release machinery in this repository is worth reading because there is more of it than a small project needs.

There is a release script that runs a semantic-release binary through the package runner. There is a release configuration file and a release manifest file at the root, which belong to a different tool, one that normally derives the version and writes the manifest itself. And there is a separate changelog script that runs a changelog generator over all commits and redirects the output into the changelog file.

So three mechanisms are present: a commit-driven publisher, a manifest-based versioning tool, and a hand-invoked changelog regeneration. Any two of those would be defensible.

The observable consequence is in the tag names. The recorded releases are prefixed with the package name before the version, which is the naming convention of the manifest-based tool, not the default convention of the commit-driven publisher. Whatever runs in practice is producing the other tool's tag format, and the two configurations in the repository are not both describing what happens.

It does not affect a consumer, who sees a normal sequence of published versions. It affects a contributor, who has two configuration files implying two different mechanisms and no statement of which one owns the version.

## The plugin lints other people's markdown and checks its own rule table

The lint step fans out to four sub-tasks, and two of them are about documentation rather than code.

One runs a markdown linter across every markdown file in the repository. Another runs the documentation generator in check mode. The remaining two are the JavaScript lint and the type check described above.

The documentation generator is the more interesting one, and the readme shows what it produces. The rules table sits between auto-generated markers in the source, and every rule has its own page under a rules directory with a description in the table linking to it. Running the generator without the check flag rewrites that table from the rule metadata, and running it with the flag fails if the committed table does not match what the rules would generate.

So the rule list cannot drift from the rules. That is a small discipline with a large payoff in a plugin whose entire value is the rule list, and it is the reason the table has a consistent shape across fifteen entries with per-rule documentation links.

There is also a pre-commit hook wired through a git hooks package, running the formatter and the fixer over staged JavaScript, TypeScript, markdown and workflow files. So a contributor gets formatting and lint fixes applied at commit time, and the documentation check runs in the full lint rather than in the hook.

A markdown linter in a lint plugin is slightly incongruous, and it is also the clearest signal of how seriously the project takes its own documentation.

## Conclusion

Use eslint-plugin-security as a checklist generator rather than as a gate, because that is how the project describes it, and because fifteen always-on rules over idiomatic Node will produce findings faster than a team can triage them. Four things to know. Where the noise comes from: one rule flags all four equality operators including the strict pair, which is the safe comparison, and two rules cover regular expressions, splitting a statically-written unsafe pattern from one built at runtime. So the first pass over an existing codebase will be long, and the value is in the categories rather than in the counts. That the types are bundled but still carry a floor, either a linter at 9.10 or newer, which is the first release shipping its own types, or a separate v9 type package alongside an older one, and older installations have a stub type package to uninstall. That the deprecated configuration path uses a differently named preset, so a migration gets a renamed preset rather than the same preset in a new format. And that the plugin lints its own documentation, generating the rule table from rule metadata and checking it, so the list you read is not a hand-maintained page that can rot. Treat the output as a review queue with a budget rather than as a build failure you have to clear before shipping.

## FAQ

### How do you install eslint-plugin-security?

As a development dependency, with `npm install --save-dev eslint-plugin-security` or `yarn add --dev eslint-plugin-security`. It provides a flat config preset requiring ESLint 8.23 or newer, and a deprecated legacy preset under a differently named config.

### How many rules does eslint-plugin-security have?

Fifteen, and all fifteen are set in the recommended configuration, with none configured as warning-only. The table on the readme is generated from the rule metadata between auto-generated markers and checked by the lint step, so it cannot drift from the rules.

### Do I need to install type definitions for eslint-plugin-security?

No, they ship with the package. They are written against ESLint's version 9 definitions, so you need ESLint 9.10 or newer, which is the first release shipping its own types, or the v9 type package alongside an older linter. A stub type package installed from DefinitelyTyped should be removed.

### What does eslint-plugin-security check for?

Node and JavaScript security hazards: non-literal requires, evaluating a variable, non-literal filesystem filenames, child process execution with non-literal arguments, variable-key property access, pseudo-random byte generation, unsafe and dynamically built regular expressions, deprecated buffer construction, loose and strict equality comparisons, and invisible or bidirectional Unicode characters in source.

## Sources

- [eslint-community/eslint-plugin-security on GitHub](https://github.com/eslint-community/eslint-plugin-security)
- [Issues](https://github.com/eslint-community/eslint-plugin-security/issues)
- [License: Apache-2.0](https://github.com/eslint-community/eslint-plugin-security/blob/main/LICENSE)
- [README](https://github.com/eslint-community/eslint-plugin-security/blob/main/README.md)
- [Releases](https://github.com/eslint-community/eslint-plugin-security/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/eslint-community-eslint-plugin-security
