Skip to content

[chore][core] enable security lint rules - #7925

Open
ar2rsawseen wants to merge 3 commits into
masterfrom
chore/security-lint-rules
Open

[chore][core] enable security lint rules#7925
ar2rsawseen wants to merge 3 commits into
masterfrom
chore/security-lint-rules

Conversation

@ar2rsawseen

Copy link
Copy Markdown
Member

Turns on a set of security-relevant ESLint rules. Every rule was measured against this
tree first, and the count sits in .eslintrc.json next to each decision so the trade is
on the record rather than assumed.

Enabled at error — 0 reports each, so the cost is only a forward gate

no-proto, no-implied-eval, no-new-func, no-script-url,
security/detect-child-process, security/detect-eval-with-expression,
security/detect-pseudoRandomBytes.

detect-child-process is the interesting one. It exempts the destructured
const {spawn} = require('child_process') that 24 files here use, and fires on a
non-literal first argument to exec() — which is exactly the pattern CLAUDE.md tells
people to avoid. There are none today, so it costs nothing and holds the line.

Enabled at warn — real findings, but clearing them is a code change

rule reports where
security/detect-unsafe-regex 26 across 16 files mostly api/utils/common.js
no-unsanitized/property 14 across 6 files non-literal innerHTML writes in frontend code

Each needs a read to separate a real problem from a bounded one, so they are surfaced
rather than either blocking the build or being silenced at 40 sites.

Left off, with the measurement recorded

guard-for-in — 454 reports here. Measured against the tree, 6.2% of them (28) sit
on a loop that actually contains a prototype-reaching write, and those 28 are found by
the same two predicates no-prototype-pollution-sink already uses. So it adds no
coverage over that rule while adding 454 reports, and it misses the sinks that motivated
it, because a lone if/else loop body satisfies it.

security/detect-object-injection — fires on nearly every obj[key] read.
security/detect-non-literal-fs-filename — 145 reports, dominated by legitimate path
building in Gruntfile, countlyFs and the upload handlers.
security/detect-non-literal-require — 19 reports, all the plugin loader doing what a
plugin loader does.

CI

The ESLint step installs its own eslint rather than using devDependencies, so the two
new plugins are named on that line too; without it the run fails with "Failed to load
plugin". They are also added to devDependencies so a local npm install covers them.

Verification

npx eslint . — the exact command CI runs — exits 0 with 0 errors. Warnings go 81 -> 121.
Re-checked with the versions pinned in the workflow (eslint 8.57.0, security 3.0.1,
no-unsanitized 4.0.2), same result.

Test-merges cleanly with #7923.

🤖 Generated with Claude Code

ar2rsawseen and others added 3 commits August 13, 2026 09:09
Every rule here was measured against this tree before being turned on, and the
counts are recorded in .eslintrc.json beside each decision.

At "error", because each reports nothing today and so costs only a forward gate:
no-proto, no-implied-eval, no-new-func, no-script-url,
security/detect-child-process, security/detect-eval-with-expression,
security/detect-pseudoRandomBytes.

detect-child-process is worth a note: it exempts the destructured
`const {spawn} = require('child_process')` that 24 files use, and fires on a
non-literal first argument to exec(), which is the anti-pattern CLAUDE.md names.

At "warn", because the findings are real but clearing them is a code change
rather than a config change: security/detect-unsafe-regex (26 across 16 files,
mostly api/utils/common.js) and no-unsanitized/property (14 innerHTML writes
across 6 frontend files). Carried as warnings rather than silenced at 40 sites.

Left off, with the measurement in the config so the next person need not redo it:
guard-for-in reports 454 here and only 6.2% of those (28) sit on a loop that
actually contains a prototype-reaching write. Those 28 are caught by the same two
predicates no-prototype-pollution-sink uses, so the rule adds no coverage over it
while adding 454 reports, and it misses the sinks that motivated that rule because
a lone if/else loop body satisfies it. Also off: detect-object-injection (fires on
nearly every obj[key]), detect-non-literal-fs-filename (145, dominated by
legitimate path building) and detect-non-literal-require (19, all the plugin loader).

The ESLint CI step installs its own eslint instead of using devDependencies, so the
two new plugins are named there as well; without that the run fails to load them.

Verified: `npx eslint .` exits 0 with 0 errors, warnings 81 -> 121.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"^4.0.2" also satisfies 4.1.5, which declares peer eslint "^9 || ^10". Against this
repo's eslint ^8.56.0 npm resolves that to ERESOLVE and `npm install` exits non-zero,
which took out the NPM install step of the lint job and every job downstream of it.

4.0.x is the last line peering "^6 || ^7 || ^8", so "~4.0.2" holds it there. The
workflow install line was already pinned to an exact 4.0.2 and was never affected,
which is why the ESLint step itself passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
package.json gained the plugins but the lock did not, and this repo commits one.
Anything running npm ci then fails on the mismatch before it installs anything.

Purely additive: the two plugins plus regexp-tree and safe-regex, 48 lines, no
deletions. `npm ci --ignore-scripts --dry-run` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant