[Security 9/9] Backend ESLint security rules, tiered to stay credible
From the PR description
[Security 9/9] Backend ESLint security rules, tiered so the gate stays credible
Part of the split of #227 into single-topic PRs. Index: tracking comment on #227.
TL;DR
Add eslint-plugin-security in a flat config over backend/src, wired to a CI job. Rules that only fire on genuinely unsafe APIs (eval with an expression, new Buffer, pseudoRandomBytes, buffer noassert) are errors that block CI; heuristic detectors (object injection, non-literal fs/regex, timing attacks) are warnings that inform review. Currently 0 errors / 131 warnings on main.
Risk to user data
Severity: preventive. Static analysis doesn't fix a live breach; it stops whole classes of the bugs the other eight PRs fix by hand from being reintroduced. pseudoRandomBytes for a token, eval on user input, an unauthenticated new Buffer(n) - these are the raw material of the SSRF, crypto, and injection issues addressed elsewhere in this split. A gate catches them at PR time instead of in production.
Flows affected
- CI only (
.github/workflows/security-lint.yml,npm run lint). - No runtime code change.
Attack precedent - on the process
The failure mode here isn't an exploit, it's alert fatigue: a linter that cries wolf gets ignored, then disabled, then catches nothing. The security detectors are heuristic and false-positive on legitimate code (e.g. detect-object-injection fires on any obj[variable]). A gate that's 90% noise trains reviewers to rubber-stamp.
Possible fixes, and what we chose
| Option | Verdict |
|---|---|
| All security rules as errors | 131 mostly-false-positive failures → the gate gets disabled within a week. Rejected. |
| All as warnings | Never blocks anything, so a real eval-on-expression sails through. Rejected. |
| Turn on only the high-precision rules | Better, but loses the heuristic detectors' review value entirely. |
| Tier by precision: unsafe-API rules = error, heuristics = warning | Chosen. The blocking tier is trustworthy (near-zero false positives), so a red build means something; the warning tier still surfaces suspects for human review without gating. |
flowchart TD
R["eslint-plugin-security rule fires"] --> P{fires only on a<br/>genuinely unsafe API?}
P -- "yes<br/>(eval-expr, new Buffer,<br/>pseudoRandomBytes, noassert)" --> E["ERROR → fails CI"]
P -- "no<br/>(heuristic: object injection,<br/>non-literal fs/regex, timing)" --> W["warning → informs review,<br/>never blocks"]
The principle generalizes past linters: a security gate's credibility is a resource you can spend, and a noisy blocking gate spends it to zero. Precision-tier before you make anything blocking.
What's in this PR
backend/eslint.config.mjs- flat config, tiered rules (new).backend/package.json+ lockfile - eslint toolchain (lockfile regenerated on currentmain)..github/workflows/security-lint.yml- CI job runningnpm run lint.
Kept as its own workflow file so it composes with the existing
ci.ymlrather than conflicting. The config is the amal66 fork'sapps/apiconfig minus its fork-specificno-consolerule (this backend logs viaconsole).
Reading
Our analysis
Add tiered backend security linting — read the full analysis →
Think the analysis missed something the PR description covers?
Capture this PR into my fork
Download a Markdown prompt that tells Claude how to port every
commit in this PR into your working tree. Run it via
claude -p < capture-pull-252.md from
inside the repo you want the changes in.