eslint-plugin-secure-coding@3.5.0
3.5.0
Minor Changes
-
#372
a7520c8Thanks @ofri-peretz! - Dropdetect-object-injectionfrom therecommendedpresetMeasured over
express+axios+sequelize, the rule fired 535 times —
85% of everythingrecommendedreported on those three repos (632 total).
528 of the 535 had no taint indicator anywhere on the reported line:this.dataValues[updatedAtAttrName] = ... // sequelize where[field] = insertValues[field]; // sequelize Axios.prototype[method] = generateHTTPMethod(); // axios
That is ordinary internal object manipulation, not attacker-controlled key
access. Without the rule,recommendedreports 97 findings on the same corpus
instead of 632.This is a design limit rather than a tuning gap. The rule reports every
computed key that fails to match one of its hand-maintained "safe" heuristics,
so on real code the default answer is "report". Inverting that — report only
when the key is reachable from a taint source — is dataflow analysis the rule
does not perform, and the rule's own fixtures contradict it (obj[config.key]
is asserted as a violation, which is exactly the axios false positive).The rule is unchanged, still exported and still documented. Teams that want the
paranoid sweep can enable it explicitly and triage the output. It is no longer
handed to consumers as a default, because at this precision it does not protect
anyone — it teaches them to disable the plugin.No rule behaviour changes; this only affects what
recommendedturns on.
Patch Changes
-
#323
4d6114dThanks @ofri-peretz! -detect-object-injection: decide numeric keys by provability, not by variable name.The rule treated a key as a safe array index when the identifier was called
i,j,k,index,idx,norlen. That both missed real numeric
indices (result[dstOffset++],arr[lastIndex],buf[stride * n]) and would
have been fooled by a string-valued variable that happened to be namedn.isNumericKeynow recognises the shapes that are numeric by JS semantics
regardless of what any identifier holds:++/--(ToNumeric), unary-and
~,**,+when both operands are themselves provably numeric, and a
conditional whose arms both are. A numeric key can never be the string
__proto__/prototype/constructor, so these cannot pollute a prototype.Also added: a key built on a string literal prefix (
nodeProperties['node' + i])
is safe, because the result always begins with that prefix and so can never
equal a dangerous name. Only a prefix counts — a trailing literal (arr[a + 1])
still reports, since+runs through string concatenation and the rule's threat
model covers unintended-key writes beyond the three prototype names.Measured on the ILB-Edge corpus (three.js + webpack + lodash): 1,753 → 1,621
findings. Recall is unchanged by construction — every suppressed shape is one
where the key provably cannot be a dangerous string. -
#323
4d6114dThanks @ofri-peretz! -detect-object-injection: resolve index expressions through scope, and drop the index-name allowlist.Three changes, all replacing naming heuristics with facts about the code:
Operands resolved through scope.
values[valueStart + k]is ordinary index
arithmetic, but+between two identifiers proves nothing on its own. Each
operand is now resolved to its declaration: if every value the variable ever
receives is provably numeric, the sum is numeric. Deliberately conservative — a
parameter, afor..ofbinding, or a single non-numeric assignment anywhere
leaves the variable unproven and the access still reports, so the analysis can
only fail to clear a safe access, never clear an unsafe one.A literal on either side of
+disqualifies the dangerous names.
array[offset + 1]always ends with1andobj['node' + i]always begins
withnode; neither can equal__proto__,prototypeorconstructor— the
rule's owndangerousProperties. This is the dominant real form once the
offset is a function parameter, where the declaration proves nothing. Scoped to
dangerousProperties, so narrowing that option narrows what disqualifies.The index-name allowlist is gone. Treating a key as safe because it was
namedi,j,k,index,idx,norlenwas unsound in both
directions: it silently clearedfunction put(o, k) { o[k] = 1 }, wherekis
an untrusted parameter that merely looks like a counter — a false negative — and
it missed every real index not on the list (offset,lastIndex,stride).
Scope resolution covers the genuine counters and refuses the parameters.Math.floor(...)and the otherMathmethods are now recognised as numeric,
which is how indices are actually computed (Math.floor(Math.random() * n)).Measured on the ILB-Edge corpus: the index-arithmetic class drops 275 → 55
(−80%), total Edge findings 2,759 → 2,539. The new false-negative lock
(o[k]on a parameter) reports where the old allowlist stayed silent. -
#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 threw
Cannot 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. -
#457
742d76fThanks @ofri-peretz! -no-improper-sanitizationno longer reports developer-authored output or code
that already escapes.This rule produced 42 of the 411 findings on the 13-repo wild corpus — its
largest single contributor — and one of the 16 ILB-CWE-Corpus false positives.Removed the custom-sanitizer check (8 wild findings, 1 corpus). It reported
any call to a function whose name contained sanitize/escape/clean when an
argument's printed text containedreq./body/query/params/input/data
— sosanitizeForLog(req.body.username)was a finding. That is the correct
code, and the claim "custom sanitizer may be incomplete or bypassable" was made
about an implementation the check never read. ThedangerousSanitizerUsage
messageId is gone with it.Widened the authored-text exemption (34 wild findings). A literal reaching
res.send/write/jsonis exempt when no tainted leaf reaches the sink with
it, rather than only when it is the direct argument. Now covered: concatenated
literals,['<li>', '</li>'].join('\n'), values passed through a named
sanitizer (escapeHtml,DOMPurify.sanitize,he.encode), and object
literals served as JSON.The #441 false negatives stay closed —
res.send(req.query.name || '<p>x</p>'),
the ternary form, and any tainted operand still report, as do computed callees,
deeper member chains, and template literals carrying expressions. -
#441
60686f9Thanks @ofri-peretz! - Stopno-improper-sanitizationreporting static developer-authored HTMLA bare string literal reaching
res.send()/res.write()/res.json()was
reported as CWE-116 whenever it contained<or>, with no requirement of
interpolation or user input. Express's ownexamples/auth/index.js:89—
res.send('… <a href="/logout">logout</a>')— was one of 188 findings the
recommended preset produced on Express's reference code (#398).The rule already applied the opposite reasoning on the
innerHTMLpath
("static developer-authored HTML normally has no taint source"); that
exemption now covers the response-output sinks too. Dangerous markup
(<script>, inlineon*=handlers,javascript:) still reports even when
hardcoded, because there the literal is itself the vector. -
#459
8b3ce82Thanks @ofri-peretz! -no-unchecked-loop-conditionno longer infers user input from identifier names.Taint was decided by substring-matching identifiers — and the printed text of
whole expressions — against
['req','request','body','query','params','input','data'], with
includes('input')andincludes('data')OR-ed in unconditionally. So
metadataMap,dataSource,queryBuilder,LoggerRequestIdHeadersand a
localqueryobject all read as attacker-controlled.The guess also propagated: a variable whose initializer text mentioned one of
those names joined the taint set, soconst found = coll.find(query)made
foundtainted and every laterfor (const r of found)a finding.Taint now starts only at a real request object (
req,request,ctx,
context,event) and spreads by assignment, seeded from the initializer's
AST rather than its printed text.req.queryis evidence;queryis a name.28 findings across express, ultimate-backend and ack-nestjs-boilerplate drop to
1 — a genuine true positive iteratingctx.headers. Request-derived loops
still report, directly and through assignment. -
#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 to
scripts/__tests__/eslint-peer-floor.test.tsso the two cannot drift again. -
#309
237a6b0Thanks @ofri-peretz! -meta.hasSuggestionsnow matches what each rule actually emits.ILB-Remediation measured 27 rules where the declaration and the implementation
disagreed: 22 declaredhasSuggestions: truewithout ever passingsuggest:
tocontext.report()(IDE quick-fix menus advertising remediation that never
arrives), and 5 emittedsuggest:without the declaration (latent — ESLint
throws on that combination as soon as one of those suggestions carries a real
fixer).eslint-plugin-mongodb-securitygains four real suggestions where the rewrite
is mechanical:require-lean-queries— appends.lean()no-unbounded-find— appends.limit(100)no-debug-mode-production— rewrites the flag toprocess.env.NODE_ENV !== 'production'require-tls-connection— adds (or flips)tls: truein the connection options
Every other dead declaration was removed rather than faked. A workspace lock
(scripts/__tests__/suggestions-meta-lock.test.ts) now fails CI on either
direction of the drift. -
#417
658368aThanks @ofri-peretz! - Two more false-positive classes from the whole-ruleset sweep.no-timing-unsafe-compare: 108 → 12 findings. Two causes.An existence check is not a secret comparison —
if (token !== undefined),
hash === null,signature.length === 0. A timing attack needs an
attacker-supplied operand on the other side; a sentinel leaks nothing.And
keywas in the default secret patterns, substring-matched. It hitkey,
firstKey,keys, and every AST walker'skey === 'text'— 88 findings on this
repo, none of them secrets. The names that actually denote a secret (apiKey,
privateKey,encryptionKey,accessToken, …) are listed in full and still
fire; a project that really does compare a barekeycan add it back via
secretPatterns.Word-boundary matching was tried first and dropped: it fixed
firstKeybut
stopped matchingreq.headers.authorization, trading one false positive for a
worse false negative.no-xxe-injection: 76 → 1 finding.parsewas treated as an XML method
name, soJSON.parse(fs.readFileSync(file, 'utf-8'))reported CWE-611. The
XML-specific names (parseFromString,parseXmlString,parseXML,
parseString) still match on the name alone; a bareparsenow has to be
positively identified as XML by its receiver, which dropsJSON.parse,
Date.parse,path.parseandurl.parse. Allowlist rather than denylist, so a
futurecsv.parseis silent by default.Every class is locked as
validcases and verified by reverting the guard. -
#422
41b9903Thanks @ofri-peretz! -no-xpath-injectionno longer treats every path join as XPath construction.The heuristic for "does this string concatenation look like XPath" was
includes('/') || includes('['), which matches every path join, URL build and
array index in existence. Measured on the Interlace monorepo, it reported
CWE-643 on:return fullPath.replace(baseDir + '/', '');
XPath has syntax of its own, so the gate now requires some of it: the descendant
axis (//), an attribute predicate ([@id=), an explicit axis (child::), the
node tests and functions (text(),node(),contains(,starts-with(,
local-name(,position()), or a location step carrying a predicate (/user[)
— the form that has no//.Verified in both directions: path joins, URL builds and array-index strings go
silent, while"//user[name='" + input + "']","/root[@id='" + input + "']"
and"/root/user[" + input + "]"all still report.Known limitation, unchanged and now documented in the source: the
variable-declaration path still matches on the namepath, so
let path = template;reports. Dropping that keyword was tried and reverted —
it also stoppedlet searchPath = userInput;firing, and by name alone the two
are indistinguishable. Separating them needs the declaration's use to reach an
XPath sink, which is the data-flow analysis these rules avoid. -
Updated dependencies [
b59e984,5ecf4d1,4794017]:- @interlace/eslint-devkit@1.11.0