eslint-plugin-security runs as part of the existing pnpm lint gate, as blocking errors (not warnings)
pnpm lint passes green across both apps with the security plugin active — existing detect-object-injection / fs-filename noise is triaged (rule-tuned or targeted eslint-disable with justification), not left red
path
provides
contains
eslint.config.js
eslint-plugin-security recommended config folded in before prettierConfig
eslint-plugin-security
path
provides
contains
package.json
eslint-plugin-security added to root devDependencies
Fold eslint-plugin-security into the existing root flat ESLint config (D-03) so its rules run as blocking ERRORS inside the current `pnpm lint` step, and triage the resulting violations across the existing codebase so the gate goes green.
Purpose: The phase 13 ESLint gate already runs on every PR in fast-checks. Adding a static security lint here costs nothing extra in CI (same install, same step). The plugin is heuristic and noisy — detect-object-injection fires on every obj[key] (pervasive in Drizzle ORM and TS generics) and detect-non-literal-fs-filename can fire on dynamic path construction. The user explicitly chose error over warn, accepting that triage of existing code is expected work, not a blocker.
Output: eslint-plugin-security in root devDependencies (pinned), the flat-config block, and whatever targeted suppressions / rule-tunes are needed to make pnpm lint green. Consumed by 16-05 (no new ci.yml step — the existing lint step now enforces it; 16-05 only documents the fold).
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-CONTEXT.md
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-PATTERNS.md
@eslint.config.js
Task 1: Install eslint-plugin-security and fold it into the flat config
- package.json (root devDependencies block — lines 18-27; existing pinned eslint tooling versions)
- eslint.config.js (the whole file — section 5 prettierConfig MUST remain last; the existing `files: ['apps/**/*.{ts,tsx}']` block pattern at lines 27-43)
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-PATTERNS.md (eslint.config.js section — exact import + spread placement before prettierConfig; detect-object-injection guidance)
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md (eslint-plugin-security Integration section — version 4.0.1 (or 3.0.1), flat-config wiring, ESLint 9.39.4 compatibility, the 15-rule table)
Add eslint-plugin-security to root devDependencies pinned to an EXACT version (4.0.1; or 3.0.1 if the executor prefers more bake time — both are flat-config compatible with the pinned ESLint 9.39.4). Use `pnpm add -D -w eslint-plugin-security@` so pnpm-lock.yaml updates. Do NOT upgrade ESLint. In eslint.config.js: add `import pluginSecurity from 'eslint-plugin-security';` to the import block (lines 7-11), and insert a NEW config block with `files: ['apps/**/*.{ts,tsx}']` that spreads `...pluginSecurity.configs.recommended` and its `...pluginSecurity.configs.recommended.rules` — placed AFTER section 4 (disableTypeChecked) and BEFORE `prettierConfig` (which must stay the last element). Add a section-header comment ("eslint-plugin-security: blocking errors per D-03"). Do not yet add per-rule overrides — Task 2 decides those after measuring noise. Commit: `chore(16-03): add eslint-plugin-security to root flat config (D-03)`.
node -e "const p=require('./package.json'); if(!p.devDependencies['eslint-plugin-security']) throw new Error('not in devDependencies'); console.log('dep OK')" && grep -q "pluginSecurity" eslint.config.js && grep -nq "prettierConfig" eslint.config.js && echo CONFIG-OK
- eslint-plugin-security present in root devDependencies at an exact pinned version; pnpm-lock.yaml updated
- eslint.config.js imports pluginSecurity and spreads configs.recommended in an `apps/**/*.{ts,tsx}` block placed before prettierConfig
- prettierConfig remains the final element of the exported config array
- ESLint version unchanged (still 9.39.4)
The security plugin is installed and registered in the flat config, before prettier, without touching the ESLint pin.
Task 2: Triage security-rule violations until pnpm lint is green
- eslint.config.js (the security block added in Task 1)
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md (Triage Strategy section — Option A: disable detect-object-injection globally with a justification comment + inline disable at true risk sites; Option B: keep error + annotate each site; other high-noise candidates: detect-non-literal-fs-filename, detect-possible-timing-attacks)
- apps/api/src (Drizzle ORM bracket-access and generic patterns that trigger detect-object-injection)
- apps/pwa/src (any dynamic bracket access / fs-like patterns)
Run `pnpm lint` and capture every security/* violation grouped by rule. For each rule decide: (a) genuine risk → fix the code; (b) whole-codebase false positive (e.g. security/detect-object-injection on Drizzle/TS-generic bracket access where the key is schema-derived or zod-validated, not user-controlled) → disable that single rule in the eslint.config.js security block with an inline comment justifying why (note that real user-controlled key risks are guarded by zod validation); (c) a small number of site-specific false positives → add `// eslint-disable-next-line security/ -- ` at each site. Prefer the minimal change that keeps the maximum number of rules at error: disable only the rules that are pervasively false-positive (likely just detect-object-injection, possibly detect-non-literal-fs-filename), and annotate individual sites for the rest. Re-run `pnpm lint` until it is green with `--max-warnings 0`. Do NOT introduce blanket `/* eslint-disable */` file headers. Commit: `chore(16-03): triage eslint-plugin-security findings to green`.
pnpm lint
- `pnpm lint` exits 0 across both apps with the security plugin active
- Any globally disabled security rule has an inline justification comment in eslint.config.js (no silent `off`)
- No blanket file-level `/* eslint-disable */` headers were added; suppressions are rule-specific with `-- justification`
- The majority of the 15 security rules remain at error (only pervasively-false-positive rules are disabled)
pnpm lint is green with eslint-plugin-security enforcing as errors; suppressions are minimal and justified.
<threat_model>
Trust Boundaries
Boundary
Description
developer source → committed code
Static security lint inspects source at code-analysis time, before it ships
user-controlled input → object/property access
detect-object-injection targets this; real risk only when the key is attacker-controlled and unvalidated
STRIDE Threat Register
Threat ID
Category
Component
Disposition
Mitigation Plan
T-16-08
Tampering / Information Disclosure
Insecure code patterns (eval, child_process with variables, unsafe regex/ReDoS, pseudo-random crypto) introduced in source
mitigate
D-03: eslint-plugin-security rules run as blocking errors in the lint gate (Tasks 1-2), failing the PR on flagged patterns
T-16-09
Elevation of Privilege
Prototype-pollution / object-injection via user-controlled bracket keys
mitigate
detect-object-injection findings triaged: real user-controlled sites are zod-validated; the rule is disabled globally ONLY because the remaining hits are schema-derived keys (Task 2 justification), not because the risk is ignored
T-16-10
Repudiation
Suppression comments hide a genuine vulnerability without accountability
accept
Every suppression carries a -- justification; no blanket file disables (Task 2); residual risk is the reviewer trusting the justification, accepted for a two-person repo with PR review
</threat_model>
- `pnpm lint` → exit 0 (both apps, --max-warnings 0)
- `grep -n "security/detect-object-injection" eslint.config.js` → if present, an adjacent justification comment exists
- eslint-plugin-security pinned in package.json devDependencies; pnpm-lock.yaml reflects it
<success_criteria>
eslint-plugin-security folded into the existing flat config as blocking errors, prettierConfig still last
pnpm lint green with the plugin active
Suppressions are rule-specific and justified; the ESLint pin is untouched
</success_criteria>
Create `.planning/phases/16-ci-dependency-audit-and-security-checks/16-03-SUMMARY.md` when done.