Changelog
Release history and version updates for eslint-plugin-express-security
Live from GitHub
This changelog is fetched directly from CHANGELOG.md on GitHub and cached for 2 hours.
3.1.1
Patch Changes
-
#563
20b22aaThanks @ofri-peretz! -no-xpath-injectionnow needs evidence rather than names, and every rule carries a CVSS.no-xpath-injectionhad a false positive and a false negative in the same rule. It reportedconst QueryValidateSchema = QueryInputSchema— a Zod schema in a file with no XPath — because the declaration name contained "query" and the initialiser name contained "query" and "input". It stayed silent onxpath.select("//user[@id='" + id + "']", doc)becauseidis not in its taint-name list, even though the string is XPath, the sink is proven, and part of the expression is dynamic. A declaration must now reach an evaluator, and a proven sink is evidence in its own right. Concatenations are also flattened and reported once at the outermost node instead of at every nesting level. Measured across 20 open-source projects: 66 findings down to 42.CVSS coverage goes from 80/121 rules to 121/121.
formatLLMMessagealready enriched fromCWE_MAPPING; the table was missing 30 of the CWEs the rules declare. Lookup now also tolerates zero-padded ids (CWE-020matchesCWE-20), and 24 rules whose documented CVSS disagreed with the class score now follow the sourced table. -
Updated dependencies [
20b22aa]:- @interlace/eslint-devkit@1.16.0
3.1.0
Minor Changes
-
#549
0e9a929Thanks @ofri-peretz! -require-rate-limitingmoves fromwarntoerrorinrecommended.Its findings concentrate in single-purpose demo apps —
express/examples/*— where rate limiting genuinely is not wanted. Those are correct detections, not false positives, and the reporting posture is now written down: we report, and the consumer scopes. Someone who does not want this in a demo directory disables it there, explicitly, where the decision is visible in their config. A path exclusion shipped in the preset would be a decision made silently on behalf of every consumer, including the ones whoseexamples/directory is production code.This resolves an inconsistency
.agent/wild-ground-truth.jsonhad already flagged as "the actionable part":require-helmet, the same shape with the same finding profile, has always beenerrorwhile this sat atwarn.If you were relying on this being a warning, set it back in your config:
rules: { 'express-security/require-rate-limiting': 'warn' }
3.0.0
Major Changes
-
#548
d86a8d8Thanks @ofri-peretz! - All eight rules now require an Express app in the file, and one rule stops reporting a non-issue.Correction (2026-08-13). The entry published with 3.0.0 said only
addLimitwas removed, and thatmissingLimitstill covered the site. That was wrong — both messageIds are gone, and the reason is the one below. The package itself is correct; only this description was. No republish: the text is fixed here and ships with the next release.Breaking:
require-express-body-parser-limitsno longer emitsmissingLimitoraddLimit. It no longer reports a missing limit at all.express.json()with no options is not unbounded. All four parsers —json,urlencoded,raw,text— shiplimit: '100kb'by default, in Express 4 and 5 alike. The old message ("Missing Body Parser Limit … attackers can exhaust server memory") was a claim about a default that does not exist: 100kb is far below every threshold this rule enforces, so it reported a configuration it would have accepted had it been written out. All seven findings on the 8-repo corpus were that shape, includingapp.use(express.urlencoded({ extended: true })).What remains is
excessiveLimit— an explicit limit abovemaxLimit, which is also the only form an attacker can steer. It now also catches the numeric spelling (limit: 52428800) that the string-only comparison missed entirely.Suppressions naming
missingLimitoraddLimitare now unused; delete them.Two new internal probes carry the change.
app-compositionanswers "is this receiver an Express app, and where is its middleware stack assembled?", sono-insecure-cookie-options,no-static-root-exposure,require-csrf-protection,require-helmetandrequire-rate-limitingstop firing on any object that happens to own a matching method name.auth-evidencedoes the same forrequire-route-authentication, which was reporting routes that are authenticated by a router-level or app-level guard it could not see.no-permissive-corsgained a detection rather than losing one: it now reads an exportedCorsOptionsdeclaration, which is how a real application configures CORS, and was a false negative before.
Patch Changes
- Updated dependencies [
d86a8d8]:- @interlace/eslint-devkit@1.14.0
2.0.1
Patch Changes
-
#460
561997fThanks @ofri-peretz! -no-user-controlled-redirectno longer flags the documented safe redirectThe rule reported
res.redirect(req.query.url)on sight, with no analysis of whether the value had been validated. That meant it fired on the exact pattern Express publishes on its "Production Best Practices: Security" page, and that the OWASP Unvalidated Redirects cheat sheet recommends:app.use((req, res) => { try { if (new URL(req.query.url).host !== 'example.com') { return res.status(400).end('Unsupported redirect'); } } catch (e) { return res.status(400).end('Invalid url'); } res.redirect(req.query.url); // ← was reported as CWE-601 });Telling a reader that their documented mitigation is the vulnerability is worse than saying nothing: the natural response is to delete the check.
The rule now recognises an origin allowlist applied to the same user source —
new URL(<source>).host/.hostname/.origininside anifwhose consequent returns or throws — and stays quiet. Unguarded redirects still report, a guard on a different source still reports, and an origin check that does not bail out is still not treated as a guard.Guard detection is structural (node-by-node member-path comparison), not text comparison of printed source.
-
#529
0c51db7Thanks @ofri-peretz! -no-user-controlled-redirect: a guard inside a nested function no longer counts as a guard.The origin-guard search descended into nested functions, so a check whose
returnexits a helper rather than the request handler silenced the rule while validating nothing.
2.0.0
Major Changes
-
#482
56f5d71Thanks @ofri-peretz! - Every rule now abstains in files without local Express evidenceThe plugin had no notion of whether a file had Express in it. Measured over 107,382 files across 108 repositories: 5,921 findings, of which 4,450 (75%) were in files with no Express import —
no-missing-csrf-protectionalone contributed 3,556.Every rule now requires local evidence: an import /
require/ dynamicimport()ofexpress, or a(req, res, next)middleware signature.The signature arm is deliberately the three-argument form only. Two-argument
(req, res)is shared withnode:http, Next.js API routes and other servers, so accepting it would re-import the false positives this change removes. The three-argument form with a trailingnextis the Connect/Express middleware contract and essentially nothing else. Error-handling middleware(err, req, res, next)matches on the tail.The import arm alone was not enough: over the 12-repo Express corpus, 68 of 114 files containing a
(req, res)-shaped function (60%) import noexpress— route modules routinely receiveapporrouterfrom their caller.After the change the same corpus yields 1,480 findings instead of 5,921, and no recall is lost: diffed across all 1,003 files that import
express, findings are identical before and after (1,554 → 1,554, zero lost). The 44 remaining findings outside anexpressimport are, by construction, files the gate opened on the middleware signature.This is a major bump: any rule may now stay silent where it previously reported.
Minor Changes
-
#468
f5a9d0dThanks @ofri-peretz! - Three deprecated rules are no longer part of therecommendedpreset:no-missing-cors-check,no-missing-csrf-protectionandno-missing-security-headers.All three carry
deprecated: trueand name a replacement that is already in the same preset at'error'—no-permissive-cors,require-csrf-protectionandrequire-helmetrespectively. Every adopter was running each check twice and getting two findings on one line, where silencing either leaves the other.Measured on the 13-repo wild corpus (~1,900 files of real Express and NestJS code): 43 findings removed, 355 → 312, and 21 CSRF locations were being reported by both rules at once.
The rules remain exported, so anyone enabling them explicitly is unaffected. They are simply no longer on by default.
Patch Changes
-
#475
db73308Thanks @ofri-peretz! - Stop reporting on evidence that lives in another file, or on no LDAP evidence at allexpress-security —
require-helmet,require-rate-limiting. Both requiredexpress()and the middleware registration to be in the same file. Splitting setup intosetAppConfigurations(app)is the normal shape for any non-toy Express app, so both reported ToniR7/express-typescript-starter, which registershelmet()and its rate limiter inutils/appInitialization.tsand has both packages independencies.They now abstain once the app binding is passed to another function: the middleware stack is assembled somewhere the rule cannot see, and "no helmet here" says nothing about the application.
secure-coding —
no-ldap-injection. One branch reported any variable whose initializer printed containingreq., with no LDAP evidence required. It flaggedvar header = req.headers[field.toLowerCase()]in expressjs/morgan — an HTTP logger with no LDAP anywhere — as CWE-90 at CVSS 9.8. The "looks like a filter" guard was satisfied by the parentheses oftoLowerCase().That branch now requires the file to touch LDAP: an
ldapjs/ldapts/activedirectory/passport-ldapauthimport (ESM orrequire(), since LDAP code in the wild is largely CommonJS), or a call to one of the LDAP sink methods the rule already recognises. The rule's other branches each carry their own evidence — an LDAP method call, or a literal that parses as a dangerous filter — and are unchanged. -
#494
4c4af8dThanks @ofri-peretz! - Close two false-negative classes across every SDK-evidence gateA full false-negative audit ran every gated plugin twice over the 107-repository corpus — once with the gates forced open, once as shipped — and compared the 6,686 findings the gates silence across 5,235 files. Of those files, 134 were flagged as suspect (the SDK is imported but the gate closed anyway), and two real defect classes came out of verifying them one by one.
1. TypeScript's import-equals form was invisible to four of the five gates.
import express = require('express')is aTSImportEqualsDeclarationwhose module reference is aTSExternalModuleReference— not arequireCallExpression— so the dynamic-load arm never saw it. 82 corpus files written this way for Express alone had every rule in the plugin silenced. Onlymongodb-securityhandled it, and only because an earlier audit forced the issue. Now handled by express, lambda, postgresql and vercel-ai too.2. Deno's module specifiers were unrecognisable to all five.
npm:@aws-sdk/client-bedrock-runtimeandhttps://deno.land/x/postgres@v0.17.0/mod.tsare ordinary SDK imports in Deno and Supabase Edge Functions; the prefix made the specifier unmatchable and the whole plugin abstained on real SDK code. Both forms are now normalised before the package test.postgresql-securityalso had no dynamicimport()arm at all — alone among the five — so a file that lazily loads its driver was silenced entirely. Every other gate has carried that arm since #481.Measured, not assumed. Re-sweeping the same 119,271 files with the fixes: 198 findings recovered across 88 files (196 express, 1 postgres, 1 lambda) and zero regressions — nothing that reported before is silenced now.
The two non-Express recoveries are the clearest illustration of what was broken:
no-missing-authorization-checkon a Supabase Edge Function calling Bedrock, andno-missing-client-releaseon a Deno postgres pool driver. Both are real serverless code that the ecosystem was blind to.Verification also ruled out four groups the generous probe flagged, rather than widening the gates to swallow them:
@serverless/*and@aws-lambda-powertools/*hits were the frameworks' own source (one specifier was inside a JSDoc@exampleblock), and@payloadcms/db-mongodb/@medusajs/deps/pgwere type-only imports of adapter packages in files that never touch the driver.Each new arm ships a positive control in the plugin's
module-gate.lock.test.ts— import-equals,npm:, anddeno.land/xfor every gate, plus the dynamicimport()case for postgres — so none of them can regress silently. All four packages remain at 100% statements / branches / functions / lines. -
Updated dependencies [
574b1ae]:- @interlace/eslint-devkit@1.12.0
1.5.6
Patch Changes
-
#407
5ecf4d1Thanks @ofri-peretz! - Correct the declared ESLint floor:^8.0.0→^8.40.0.context.sourceCodelanded in ESLint 8.40. The shared devkit reads it without a fallback and 20 plugins read it directly, so on ESLint 8.0–8.39 the install resolved cleanly and then every rule threwCannot read properties of undefined (reading 'ast')at lint time — npm reported nothing, because the manifest claimed the version was supported.Measured on 8.0.0 / 8.39.0 (throw on load) versus 8.40.0 / 8.57.1 / 9.0.0 / 9.39.2 / 10.8.0 (all produce the expected finding). No runtime behaviour changes; this only makes the manifest match what the code can actually run.
-
#329
75d3497Thanks @ofri-peretz! - Test infrastructure only — no rule, config, or API behavior changes. These packages shipsrc/in their npm tarball, so the moved SDK compatibility specs technically alter the published files, hence the patch bump.The
src/__compatibility__/suites no longer run as part of each package's defaultvitestrun. They assert the export surface of the third-party SDK (express, jose, @middy/core, mongodb, @nestjs/common, pg, ai), not our rules, andsdk-compatibility.ymlalready exercises them against each SDK's@latest— the only run that produces new signal. Loading those SDK graphs on a cold module cache was measured at 82s (express) and 209s (@nestjs/common), which blew every per-file hook timeout and blocked unrelated local commits via the lefthooktests-affectedpre-commit hook. The ceiling now lives once invitest.compat.config.mts, sized off those cold numbers. -
#423
4794017Thanks @ofri-peretz! - Correct the ESLint peer range shown in the README Compatibility table.The manifest floor moved to 8.40.0, but every package README still advertised
^8.0.0 || ^9.0.0 || ^10.0.0. The README is what npm renders on the package page, so the requirement consumers actually read disagreed with the one npm enforced: an install on 8.39.x warns about a peer conflict while the README says that version is supported.The range was missed by the original sweep because a markdown table escapes the union as
\|\|, so a grep for the plain shape matched none of the 29 files.Also updates
.agent/rules/readme-structure.mdand.agent/compatibility-matrix.md, which template this table for new packages, and adds a README-vs-manifest assertion toscripts/__tests__/eslint-peer-floor.test.tsso the two cannot drift again. -
Updated dependencies [
b59e984,5ecf4d1,4794017]:- @interlace/eslint-devkit@1.11.0
1.5.5
Patch Changes
-
#411
d0cc8b6Thanks @ofri-peretz! - Ship the JavaScript without tsc's layout.Every emitted
.jsis re-written through esbuild'sminifyWhitespace, which removes indentation and line breaks. Across the ecosystem that is 3233 kB -> 2023 kB of shipped JavaScript, a 37% cut; on disk a package install drops about 28%. Indentation alone was ~32% of a compiled rule file.This is deliberately NOT minification. Identifiers keep their names, string contents are untouched, and the syntax tree is not rewritten — rule
meta(messages, schema, docs URLs) stays byte-identical, which is what the docs site and--print-configread, and a stack trace from inside a rule still names the function it came from. Full mangling would have bought another 4 kB gzipped and cost both.Verified against the published artifact: identical lint findings including message IDs, identical rule names, and zero differences across every rule's meta, messages, schema and presets.
-
Updated dependencies [
7663cfd,d0cc8b6]:- @interlace/eslint-devkit@1.10.0
1.5.4
Patch Changes
-
#383
868c4a8Thanks @ofri-peretz! - Document every rule option, and adddescriptionto the schemas that had none282 working options across 123 rules had no row in their rule's Options table, and 62 rule docs had no Options section at all. An option nobody can find is, in practice, an option that does not exist — the only difference from a dead one is that the code is there.
Schema descriptions are now the source of truth, so editors and any tooling that reads
meta.schemaget them too, not just the docs site. 75 options that had no description anywhere got one written from their own default value and the rule's stated purpose.Rule behaviour is unchanged. This is documentation plus schema
descriptionmetadata; no detection, option name, or default was touched. -
#381
74bbf60Thanks @ofri-peretz! - Load rule modules on demand instead of at plugin load.Every plugin barrel used to
requireall of its rules the moment ESLint loaded the plugin, whether or not your config enabled them.plugin.rules[id]is only ever read for rules a config turns on, so the rest was parse-and-compile cost for code that never ran.The published entry now exposes each rule behind a getter, so a rule module is read the first time something asks for it. Measured on a 7-plugin config with 34 rules enabled: 163 rule modules loaded and 251 ms of plugin load, against 34 modules and 8.5 ms — total ESLint wall time 251 ms → 109 ms. On a preset that enables most of a plugin (
node-security/recommended, 25 of 37) it is a wash, 72 ms → 65 ms. It is never slower; the win scales with how many plugins you stack and how few of their rules you use.Nothing about the plugin API changes.
Object.keys(plugin.rules)still lists every rule without loading any of them, repeated reads return the same object, and the./oxlintsub-export is the same plugin object it always was.eslint-plugin-jwtandeslint-plugin-vercel-ai-securityalso re-export their rule objects as named top-level exports, which cannot be deferred — those two keep loading eagerly. -
#381
74bbf60Thanks @ofri-peretz! - Declare what we support, load only what we usetslibis gone from every package. It was a NON-optional peer of@interlace/eslint-devkit, so all 26 plugins declared it as a dependency to satisfy that peer — 124 kB every consumer installed so twelverequire("tslib")calls could resolve. The shipped JavaScript now inlines the TypeScript helpers instead (--importHelpers falseon the emit pass that already re-writes it), costing ~9.5 kB in devkit. Zerotslibrequires remain anywhere; verified by installing every plugin with notslibin the tree and loading all 26 with every rule intact.eslint-plugin-import-nexthad a phantom dependency. Its rulesrequire("typescript")at module load, but it was declared in neitherdependenciesnorpeerDependencies— it worked only because something else in the tree happened to install it. A clean install crashed the whole plugin, not just the type-aware rules.typescriptis now a required peer, which is what the code actually needs.23 "technologies we support" declarations did nothing. Seven plugins listed their target libraries in
peerDependenciesMetawith no matchingpeerDependenciesentry, and npm ignores meta for a package that is not declared a peer — verified by installingeslint-plugin-express-securityand watching nothing install and nothing warn.eslint-plugin-jwtappeared to support six JWT libraries and formally supported none. All 23 are now real optional peers, matching the conventionpg,mongodb,prismaand the other nine already followed:plugin technologies now actually declared eslint-plugin-jwtjsonwebtoken, @nestjs/jwt, express-jwt, jose, jwks-rsa, jwt-decode eslint-plugin-lambda-security@aws-sdk/client-lambda, @middy/core, @middy/http-cors, @middy/http-security-headers, @middy/validator eslint-plugin-express-securityexpress, helmet, cors, csurf, express-rate-limit eslint-plugin-nestjs-security@nestjs/common, @nestjs/throttler, class-validator, class-transformer eslint-plugin-vercel-ai-securityai eslint-plugin-maintainability,eslint-plugin-react-featurestypescript All optional, so nothing is installed on the consumer’s behalf — the declaration is the supported-technology signal, which is exactly what it was meant to be.
A new gate compares declared dependencies against what the emitted JavaScript actually loads, in both directions: a
requirewith no declaration (works until someone installs cleanly) and a declaration nothing requires (weight every consumer pays). It understands that a dependency may exist to satisfy an optional peer of another dependency, which is whyeslint-plugin-import-nextlegitimately declaresoxc-resolverthat devkit lazily loads. -
#335
47cde07Thanks @ofri-peretz! - Fix the./oxlintsubpath export, which pointed atsrc/oxlint.js— a file no build produces.require('<package>/oxlint')threw MODULE_NOT_FOUND on every published package, while every README documented that exact wiring for oxlint'sjsPlugins. The export now points at the build output,dist/src/oxlint.js.The path was hardcoded in
scripts/generate-oxlint-shims.ts, so the generator rewrote any manual correction back to the broken value on the next drift check — fixed there rather than per package.This release also carries npm provenance: the affected packages were last published from a workstation, which has no OIDC token to attest with, so the published tarballs had no attestation. Publishing through the release workflow signs them.
-
#335
47cde07Thanks @ofri-peretz! - Fix SDK peer declarations that npm silently ignoredTwelve plugins listed their target SDKs under
peerDependenciesMetawith{"optional": true}but never declared them inpeerDependencies. npm drops anypeerDependenciesMetaentry that has no matchingpeerDependencieskey, so the metadata was inert — these packages effectively declared no SDK peer at all. Nothing warned: the failure mode of a dependency you never declared is silence.Each SDK now appears in both maps, matching the shape
eslint-plugin-pgandeslint-plugin-mongodb-securityalready use — a supported major range inpeerDependencies,optional: trueinpeerDependenciesMeta:Plugin SDK Range express-securityexpress^4.0.0 || ^5.0.0helmet^6.0.0 || ^7.0.0 || ^8.0.0cors^2.0.0csurf^1.0.0express-rate-limit^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0jwtjsonwebtoken^8.0.0 || ^9.0.0@nestjs/jwt^9.0.0 || ^10.0.0 || ^11.0.0express-jwt^7.0.0 || ^8.0.0jose^4.0.0 || ^5.0.0 || ^6.0.0jwks-rsa^3.0.0 || ^4.0.0jwt-decode^3.0.0 || ^4.0.0lambda-security@aws-sdk/client-lambda^3.0.0@middy/core^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0@middy/http-cors^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0@middy/http-security-headers^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0@middy/validator^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0maintainabilitytypescript>=4.8.4nestjs-security@nestjs/common^9.0.0 || ^10.0.0 || ^11.0.0@nestjs/throttler^4.0.0 || ^5.0.0 || ^6.0.0class-validator^0.14.0 || ^0.15.0class-transformer^0.5.0react-featurestypescript>=4.8.4vercel-ai-securityai^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0jwt-securitysame six as jwt(identical ranges) openai-securityopenai^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0@openai/agents>=0.1.0 <1.0.0anthropic-security@anthropic-ai/sdk>=0.1.0 <1.0.0@anthropic-ai/claude-agent-sdk>=0.1.0 <1.0.0gemini-security@google/genai^1.0.0 || ^2.0.0mcp-sdk-security@modelcontextprotocol/sdk^1.0.0Ranges were taken from each SDK's real release history, bounded below by the oldest major whose call shape the rules still match and above by the current major.
cors,csurf,class-transformerand@aws-sdk/client-lambdahave only ever shipped one usable major. Theairange spans v4 becauserequire-max-stepsdeliberately accepts both the v4maxStepsoption and the v5+stopWhenform. The twotypescriptentries reuse the>=4.8.4bound@interlace/eslint-devkitalready declares, since these are the same type-aware-graceful rules behind the same optional TS program.Every range admits the version this repo's
__compatibility__specs are actually tested against, so the declaration cannot drift from what CI proves.The four SDKs still on
0.x(@openai/agents, both Anthropic packages) use an explicit>=0.1.0 <1.0.0rather than a caret, because^0.115.0resolves to>=0.115.0 <0.116.0— a range narrow enough to warn on almost every real install. These rules match on call shape and never import the SDK, so the honest constraint is the pre-1.0 line, not a single minor.peer-declaration-integrity.test.tsnow locks the invariant across every workspace package: apeerDependenciesMetakey with nopeerDependenciestwin fails the suite and is named in the diff. This class had already been fixed once, in a commit that never merged — nothing went red in its absence, so the bug came back on four newly published packages. A silent failure needs a lock, not review attention.Nothing to migrate. Every entry stays optional, so no install adds a package or emits a warning when the SDK is absent. What changes is that a consumer on an unsupported major now gets a peer warning instead of nothing — which was the point of the metadata in the first place.
-
Updated dependencies [
85e57a7,74bbf60,e5d31ab,1fb1cad,d1a3d8c]:- @interlace/eslint-devkit@1.8.0
1.5.3
Patch Changes
- #364
86baa02Thanks @ofri-peretz! - Add the ecosystem and oxlint marks to the README logo row. Each plugin now leads with Interlace -> its ecosystem (node, nestjs, express, react, mongodb, postgresql, mysql, sqlite, prisma, drizzle, knex, typeorm, sequelize, lambda, vercel, jwt) -> oxlint -> ESLint; the generic quality plugins carry the row without an ecosystem mark. README-only change - no rule behaviour is affected. The patch bump is what carries the new README onto npm, which only refreshes a package README on publish.
1.5.2
Patch Changes
-
#358
1b8c0dfThanks @ofri-peretz! - Fix SDK peer declarations that npm silently ignoredSeven plugins listed their target SDKs under
peerDependenciesMetawith{"optional": true}but never declared them inpeerDependencies. npm drops anypeerDependenciesMetaentry that has no matchingpeerDependencieskey, so the metadata was inert — these packages effectively declared no SDK peer at all. Nothing warned: the failure mode of a dependency you never declared is silence.Each SDK now appears in both maps, matching the shape
eslint-plugin-pgandeslint-plugin-mongodb-securityalready use — a supported major range inpeerDependencies,optional: trueinpeerDependenciesMeta:Plugin SDK Range express-securityexpress^4.0.0 || ^5.0.0helmet^6.0.0 || ^7.0.0 || ^8.0.0cors^2.0.0csurf^1.0.0express-rate-limit^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0jwtjsonwebtoken^8.0.0 || ^9.0.0@nestjs/jwt^9.0.0 || ^10.0.0 || ^11.0.0express-jwt^7.0.0 || ^8.0.0jose^4.0.0 || ^5.0.0 || ^6.0.0jwks-rsa^3.0.0 || ^4.0.0jwt-decode^3.0.0 || ^4.0.0lambda-security@aws-sdk/client-lambda^3.0.0@middy/core^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0@middy/http-cors^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0@middy/http-security-headers^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0@middy/validator^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0maintainabilitytypescript>=4.8.4nestjs-security@nestjs/common^9.0.0 || ^10.0.0 || ^11.0.0@nestjs/throttler^4.0.0 || ^5.0.0 || ^6.0.0class-validator^0.14.0 || ^0.15.0class-transformer^0.5.0react-featurestypescript>=4.8.4vercel-ai-securityai^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0Ranges were taken from each SDK's real release history, bounded below by the oldest major whose call shape the rules still match and above by the current major.
cors,csurf,class-transformerand@aws-sdk/client-lambdahave only ever shipped one usable major. Theairange spans v4 becauserequire-max-stepsdeliberately accepts both the v4maxStepsoption and the v5+stopWhenform. The twotypescriptentries reuse the>=4.8.4bound@interlace/eslint-devkitalready declares, since these are the same type-aware-graceful rules behind the same optional TS program.Every range admits the version this repo's
__compatibility__specs are actually tested against, so the declaration cannot drift from what CI proves.Nothing to migrate. Every entry stays optional, so no install adds a package or emits a warning when the SDK is absent. What changes is that a consumer on an unsupported major now gets a peer warning instead of nothing — which was the point of the metadata in the first place.
-
Updated dependencies [
e8e9ee6]:- @interlace/eslint-devkit@1.7.0
1.5.1
Patch Changes
-
#338
dc25c81Thanks @ofri-peretz! - Re-publish every package so npm carries the optimised artifactNo source changed. This is a no-op patch whose entire purpose is to ship the artifact the current build already produces.
Manifests.
scriptsanddevDependenciesare now stripped from every publishedpackage.json. Neither can do anything in a consumer’s node_modules — npm never runs one and never installs the other — but they shipped in all 27 manifests, cluttered the npm page, and were read by SCA tools scanning installed manifests. No package declares a lifecycle hook, so nothing observable changes. Every published package is bumped so this applies uniformly rather than to a subset.Tarballs. 20 packages were last published before the build pipeline changed and still ship
AGENTS.md,CHANGELOG.md, JSDoc in the emitted.js, and the full generated.d.tstree:package published rebuilt saving eslint-plugin-react-features547 kB 320 kB −227 kB eslint-plugin-secure-coding653 kB 477 kB −176 kB eslint-plugin-conventions241 kB 116 kB −125 kB eslint-plugin-browser-security380 kB 291 kB −89 kB eslint-plugin-maintainability178 kB 116 kB −62 kB eslint-plugin-react-a11y232 kB 173 kB −59 kB eslint-plugin-reliability148 kB 90 kB −58 kB eslint-plugin-vercel-ai-security187 kB 130 kB −57 kB eslint-plugin-operability90 kB 43 kB −47 kB eslint-plugin-jwt140 kB 95 kB −45 kB eslint-plugin-modularity98 kB 58 kB −40 kB eslint-plugin-nestjs-security122 kB 86 kB −36 kB eslint-plugin-sqlite-security54 kB 20 kB −34 kB eslint-plugin-sequelize-security54 kB 21 kB −34 kB eslint-plugin-prisma-security52 kB 19 kB −33 kB eslint-plugin-mysql-security52 kB 19 kB −33 kB eslint-plugin-typeorm-security52 kB 19 kB −33 kB eslint-plugin-drizzle-security52 kB 19 kB −33 kB eslint-plugin-knex-security51 kB 19 kB −32 kB eslint-plugin-modernization45 kB 38 kB −7 kB Those 20 go from 3428 kB to 2169 kB — −36.7%. The remaining 7 were released after the pipeline change and only gain the manifest strip.
A new check in
scripts/check-published-artifacts.tsfails the build ifscriptsordevDependenciesever reappear in a published manifest, so the strip cannot silently regress.The dependency ranges did not need updating: every plugin pins
@interlace/eslint-devkitwith a caret that 1.6.0 satisfies, verified by a clean install of an unchanged plugin resolving devkit 1.6.0 with zero dependencies and notypescriptin the tree. -
Updated dependencies [
dc25c81]:- @interlace/eslint-devkit@1.6.1
1.5.0
Minor Changes
-
#313
1f4fc05Thanks @ofri-peretz! - Eight new rules closing the two fixable gaps found by the F#24/F#26 coverage benchmark (CWE Top 25 map + framework-depth matrix).Express — the helmet header family (the depth gap where SonarJS led 17 rules to our 14;
require-helmetonly proved the middleware was mounted, never that its protections were still on):no-disabled-helmet-protections(CWE-693) —helmet({ contentSecurityPolicy: false })and the rest of the disabled-default family, helmet 6 and 7 spellingsrequire-strict-transport-security(CWE-319) — HSTS disabled,max-agebelow the 180-day preload floor, orincludeSubDomains: falseno-unsafe-csp-directives(CWE-79 / 1021 / 311) —'unsafe-inline','unsafe-eval', wildcard sources,frame-ancestors '*', missingframe-ancestorsunderuseDefaults: false, andupgradeInsecureRequests: nullno-permissive-trust-proxy(CWE-348) —app.set('trust proxy', true), which makesreq.ipclient-controlled and every rate-limit bucket forgeable
Express — CWE Top 25 (2025) access-control adjacency (three of the four JS-applicable entries we did not cover):
require-route-authentication(CWE-306) — critical-function routes with no auth middleware and no principal read in the handlerno-client-controlled-authorization(CWE-863) —if (req.body.role === 'admin'): the check runs, and passes for anyone who sets the fieldno-idor-resource-access(CWE-639) —Invoice.findById(req.params.id)in a handler that never mentions the caller
Node — the fourth adjacency (CWE-77, generic command injection, previously covered only as CWE-78):
no-dynamic-command-string(CWE-77) — an assembled command string handed to a shell flag (spawn('bash', ['-c', …])) or to a command-runner that does not escape (execaCommand,$.raw)
In
recommended, the five structural rules ship aserror; the three access-control rules ship aswarn— their critical-path / authorization-attribute / lookup-method vocabularies are name-based, and naming heuristics never carry enforcement severity (plugin scope-audit invariant I3).
Patch Changes
1.4.0
Minor Changes
-
#292
5664efdThanks @ofri-peretz! -express-security/no-exposed-debug-endpoints— only route registrations count. The rule had a second listener that reported any bare string literal equal to a debug path (/admin,/health,/debug, …) anywhere in a file. A redirect-URL constant tripped it while authoring benchmark corpus fixtures —const ADMIN_PATH = '/admin',res.redirect('/admin')andif (req.path === '/health')were all CWE-489 "Exposed Debug Endpoint" findings, none of which registers an endpoint. That listener is gone: a literal is reported only as the path argument of an express route registration.The registration check also now covers every express routing method (
put,patch,delete,head,options,all) rather than justget/post/use, plus the chained route builder (app.route('/admin').delete(handler)), soapp.delete('/admin/users/:id', handler)is caught where it previously was not. Conversely,app.get(name)with a single argument is an application-setting lookup rather than a route registration and is no longer reported. -
#293
d6e2b3cThanks @ofri-peretz! - Seven new rules closing benchmark-corpus coverage gaps (A-lite research wave):no-host-header-in-links(CWE-640) — Host-header poisoning in password-reset/email link constructionno-error-details-in-response(CWE-209) — stack traces / raw error objects sent to clientsno-sensitive-data-in-query(CWE-598) — passwords/tokens read from GET query stringsno-user-controlled-render-locals(CWE-73) —res.render(view, req.body)template object injectionno-static-root-exposure(CWE-548) —express.static(__dirname)/serve-indexdirectory exposurerequire-case-insensitive-path-guard(CWE-178) — case-sensitive path guards bypassed by/ADMINrequire-query-type-guard(CWE-843) — string methods onreq.querymembers without type guards
In the recommended preset four ship as
error(no-host-header-in-links,no-error-details-in-response,no-user-controlled-render-locals,no-static-root-exposure) and three aswarn— the tworequire-*guard heuristics plusno-sensitive-data-in-query, which matches on parameter names and so never gets enforcement severity.
Patch Changes
-
#298
a53887fThanks @ofri-peretz! -express-security/no-missing-security-headers—.set()on a non-response receiver is not a header call. The rule matchedsetHeader/header/seton the method name alone, sourl.searchParams.set('page', '2')andapp.set('view engine', 'ejs')were reported as CVSS 7.5 missing-security-header findings — a false positive on two of the most common calls in an Express codebase. The receiver must now be an HTTP response (res/resp/response/reply, includingctx.res.set(…)andthis.response.header(…)). The same predicate gates header collection, so aContent-Security-Policystring passed to an unrelated.set()no longer satisfies the requirement for a real response in the same scope. -
#294
659f6dcThanks @ofri-peretz! - Rewritedescriptionandkeywordson every published package for npm search discovery. npm ranks on name, description, and keywords, and the registry only picks up these fields at publish — so this is metadata-only and takes effect for each package on its next release.Descriptions now lead with the search phrase. Every one starts
ESLint plugin for <the thing you'd search>instead of a brand-first or category-first framing, and names the concrete vulnerabilities the plugin actually detects. Three were corrected while doing so:eslint-plugin-import-nextclaimed "100x faster no-cycle detection". No 100x measurement exists:CLAIMS.mdrecords 3.1x end-to-end (8x in pure rule execution) on a 5,483-file React codebase, and the highest number in any benchmark result is 54.9x on the synthetic corpus. The description now states the real-codebase figure.eslint-plugin-secure-codingclaimed SQL injection, XSS and CSRF coverage — none of which are its rules. It now names what it does detect: LDAP, XPath, XXE, GraphQL and template injection, unsafe deserialization, ReDoS, missing authentication, and PII in logs.eslint-plugin-secure-coding("89 rules") andeslint-plugin-react-a11y("37 rules") hard-coded rule counts that had drifted from reality. Counts are generated intointerlace-numbers.json; hand-typed copies are removed rather than corrected.
Keywords now match the vocabulary of the plugins that rank.
eslint-plugin-security,eslint-plugin-jsx-a11y,eslint-plugin-nandeslint-plugin-importall carry theeslint/eslintplugin/eslint-plugintrio — six of our packages were missingeslintplugin, and every one now carries all three plusstatic-analysis,lintingandcode-quality. Security plugins addsast,appsecandvulnerability;node-securityandsecure-codingalso carrynodesecurity, the exact keywordeslint-plugin-securityranks on. Each plugin gained the CWE identifiers and attack names for what it detects (cwe-78command injection,cwe-22path traversal,cwe-89SQL injection,cwe-79XSS,cwe-347JWT algorithm confusion,cwe-352CSRF,cwe-943NoSQL injection), andnode-securitygained the crypto vocabulary it had been missing entirely despite absorbing the crypto rule set (crypto,cryptography,weak-hash,md5,sha1,timing-attack).No rule behavior, exports, or configuration changes.
-
Updated dependencies [
e1cdf83,659f6dc]:- @interlace/eslint-devkit@1.4.3
1.3.4
Patch Changes
-
#269
7028fe2Thanks @ofri-peretz! - docs: dual-logo README header (Interlace mark + ESLint mark side by side) and closing Interlace footer — refreshes the README rendered on npmjs.com. No runtime changes. -
Updated dependencies [
7028fe2]:- @interlace/eslint-devkit@1.4.2
1.3.3
Patch Changes
- #252
d67e395Thanks @ofri-peretz! - Fix Codecov badge showing "unknown" — switch from flag to component URL format
1.3.2
Patch Changes
- #225
34ff5a8Thanks @ofri-peretz! - CI-only: pin all coverage thresholds at 100% (integration target, merges last).
1.3.1
Patch Changes
-
#213
391dbe6Thanks @ofri-peretz! - Align every security rule'smeta.docs.cvssto the CVSS its finding actually emits. The emitted machine-readable message sources itsCVSS:xfromCWE_MAPPINGviaformatLLMMessage→enrichFromCWE, but the staticmeta.docs.cvssdocumentation field had drifted on 45 rules across these 7 plugins — e.g.no-hardcoded-credentialsdocumented9.5while emittingCVSS:9.8(the value the published article and SARIF/LLM consumers already read).This corrects the documentation metadata only — no emitted finding changes. Locked by
security-cvss-docs-consistency.lock.test.ts(cross-plugin: every security rule'smeta.docs.cvssmust equal the CVSS it emits), theno-hardcoded-credentialsrule lock (real ESLintLinteremission), and a devkitenrichFromCWEcontract test pinningCWE-798 → 9.8.Follow-up (not in scope): 50 security rules document a CVSS that never appears in any emitted message (their messages carry no CWE), and several rules emit the generic CWE score where a rule-specific score may be warranted — both change emitted output and are separate decisions.
1.3.0
Minor Changes
-
#169
ae39ec5Thanks @ofri-peretz! - feat: addno-user-controlled-redirectrule — structural CWE-601 open redirect detectionFires on
res.redirect(req.query.*),res.redirect(req.body.*), andres.redirect(req.params.*)— an AST-structural check that passes the naming-heuristic litmus test (renameres/reqto any identifier and the rule still fires, because detection is on the member-access chain, not on variable names). Severity:errorin flagship config.
Patch Changes
-
#143
213cde1Thanks @ofri-peretz! - fix(no-missing-null-checks): eliminate 53 false positives via three new narrowing patternsRules that were recognized as null guards are now correctly identified as safe:
- Truthy if guard —
if (obj) { obj.prop }— direct truthy check proves non-null. Also covers chains:if (response)protectsresponse.data.items. - Short-circuit AND —
obj && obj.prop— right side of&&only runs when left is truthy. - Ternary consequent —
obj ? obj.prop : fallback— truthy test guards the consequent.
Also: bumped
beforeAlltimeout to 30 seconds in 7 compatibility test files (__compatibility__/*.spec.ts). Native-addon packages routinely exceed the previous 10-second default on a cold ESM load. - Truthy if guard —
-
Updated dependencies [
736a5fe]:- @interlace/eslint-devkit@1.4.1
[1.2.3] - 2026-02-08
Bug Fixes
- align codecov component IDs with full package names (2831b968)
Documentation
- fix changelog header format across all packages (c3a15082)
❤️ Thank You
- Ofri Peretz
[1.2.2] - 2026-02-06
Bug Fixes
- align codecov component names and update docs components (0a59a86c)
❤️ Thank You
- Ofri Peretz
[1.2.1] - 2026-02-02
This was a version bump only for eslint-plugin-express-security to align it with other projects, there were no code changes.
Changelog
All notable changes to eslint-plugin-express-security will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Documentation
- 📘 Launched new documentation site: eslint.interlace.tools
[1.0.0] - 2025-12-29
Added
Headers & CORS Rules (4)
require-helmet- Require helmet middleware for security headers (CWE-693)no-permissive-cors- Detect wildcard CORS origins (CWE-942)no-cors-credentials-wildcard- Block credentials: true with wildcard origin (CWE-942)require-express-body-parser-limits- Require body parser size limits (CWE-770)
CSRF & Cookies Rules (2)
require-csrf-protection- Require CSRF middleware for state-changing routes (CWE-352)no-insecure-cookie-options- Detect missing Secure/HttpOnly cookie attributes (CWE-614)
Rate Limiting & DoS Rules (2)
require-rate-limiting- Require rate limiting middleware (CWE-770)no-express-unsafe-regex-route- Detect ReDoS-vulnerable regex patterns (CWE-1333)
GraphQL Rules (1)
no-graphql-introspection-production- Disable GraphQL introspection in production (CWE-200)
Presets (4)
recommended- Balanced security defaultsstrict- All 9 rules as errorsapi- HTTP/API security rules onlygraphql- GraphQL-specific rules only
Features
- LLM-optimized error messages with CWE references
- OWASP Top 10 2021 alignment (A01, A03, A05, A07)
- Middleware-aware detection (helmet, cors, csurf, express-rate-limit)
- TypeScript support with exported option types
- Comprehensive test coverage (132 tests, 93.15% line coverage)
Security
- Covers 6 CWEs: 200, 352, 614, 693, 770, 942, 1333
- Maps to OWASP Top 10 2021: A01, A03, A05, A07
View on GitHub →
Building secure JavaScript with Interlace? Star the repo to get new rules and CWE coverage as we ship them — or follow the AI-code-security benchmarks behind them.