From fa3e9baa2e6f9689242ac3efdf772f1598f63e9a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:09:16 +0300 Subject: [PATCH 1/3] [chore][core] enable security lint rules 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 --- .eslintrc.json | 47 ++++++++++++++++++++++++++++++++++++++ .github/workflows/main.yml | 5 +++- package.json | 2 ++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 162dc7b5913..7bdc734fa18 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,8 +1,55 @@ { "extends": "eslint:recommended", + "plugins": [ + "security", + "no-unsanitized" + ], "rules": { // "quotes": ["warn", "single", { "avoidEscape": true }], "require-atomic-updates": "off", + + /* ---------- Security ---------- + Counts below were measured against this tree before each rule was turned on. + A rule that costs nothing today is worth having as a forward gate; a rule + whose cost is a long cleanup is carried at "warn" with that cleanup named. + + Deliberately NOT enabled: + + guard-for-in — 454 reports here, and 6.2% of them (28) sit on a loop that + actually contains a prototype-reaching write. Those 28 are found by the same + two predicates no-prototype-pollution-sink already uses, so the rule adds no + coverage over it while adding 454 reports. It also misses the sinks that + motivated that rule, 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 of them the plugin + loader doing what a plugin loader does. */ + + // 0 reports: free forward gates. + "no-proto": "error", + "no-implied-eval": "error", + "no-new-func": "error", + "no-script-url": "error", + // 0 reports. Exempts the destructured `const {spawn} = require('child_process')` + // that 24 files use, and flags a non-literal first argument to exec() — which is + // exactly the anti-pattern CLAUDE.md calls out. + "security/detect-child-process": "error", + "security/detect-eval-with-expression": "error", + "security/detect-pseudoRandomBytes": "error", + + // 26 reports across 16 files, mostly in api/utils/common.js. Each needs a + // read to tell a real ReDoS from a bounded pattern, so it stays at "warn" + // until those are triaged rather than being disabled at 26 sites. + "security/detect-unsafe-regex": "warn", + // 14 innerHTML assignments across 6 files, all in frontend code, none of them + // literal. Same reason: real findings, but fixing them is a code change, so + // they are surfaced rather than either blocking the build or being silenced. + "no-unsanitized/method": "warn", + "no-unsanitized/property": "warn", "block-spacing": [ "error", "always" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e5165e08ab7..500373b039a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -115,8 +115,11 @@ jobs: - name: ESLint shell: bash + # This step installs its own eslint rather than using the repo devDependencies, + # so every plugin named in .eslintrc.json has to be listed here too or the run + # fails with "Failed to load plugin". run: | - npm install eslint@8.57.0 eslint-plugin-vue@9.31.0 @stylistic/eslint-plugin@2.11.0 + npm install eslint@8.57.0 eslint-plugin-vue@9.31.0 @stylistic/eslint-plugin@2.11.0 eslint-plugin-security@3.0.1 eslint-plugin-no-unsanitized@4.0.2 npx eslint . - name: Check for any external web resources diff --git a/package.json b/package.json index cdab0b9afca..6110ea32cc4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "@stylistic/eslint-plugin": "^3.1.0", "docdash": "^2.0.1", "eslint": "^8.56.0", + "eslint-plugin-no-unsanitized": "^4.0.2", + "eslint-plugin-security": "^3.0.1", "eslint-plugin-vue": "^10.2.0", "grunt-contrib-watch": "1.1.0", "grunt-eslint": "24.3.0", From 8c2c251f66add0b6115a7e6bca71d9f70511d44a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:26:51 +0300 Subject: [PATCH 2/3] [chore][core] pin eslint-plugin-no-unsanitized to the eslint 8 line "^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 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6110ea32cc4..622e1ffc64d 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@stylistic/eslint-plugin": "^3.1.0", "docdash": "^2.0.1", "eslint": "^8.56.0", - "eslint-plugin-no-unsanitized": "^4.0.2", + "eslint-plugin-no-unsanitized": "~4.0.2", "eslint-plugin-security": "^3.0.1", "eslint-plugin-vue": "^10.2.0", "grunt-contrib-watch": "1.1.0", From 144e7dd0e1c8291fa5328fef3ee5d42f0019f196 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:30:53 +0300 Subject: [PATCH 3/3] [chore][core] update package-lock for the two new lint plugins 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 --- package-lock.json | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/package-lock.json b/package-lock.json index 345a72569ff..654c44e9dfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,8 @@ "@stylistic/eslint-plugin": "^3.1.0", "docdash": "^2.0.1", "eslint": "^8.56.0", + "eslint-plugin-no-unsanitized": "~4.0.2", + "eslint-plugin-security": "^3.0.1", "eslint-plugin-vue": "^10.2.0", "grunt-contrib-watch": "1.1.0", "grunt-eslint": "24.3.0", @@ -5235,6 +5237,32 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-plugin-no-unsanitized": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.0.2.tgz", + "integrity": "sha512-Pry0S9YmHoz8NCEMRQh7N0Yexh2MYCNPIlrV52hTmS7qXnTghWsjXouF08bgsrrZqaW9tt1ZiK3j5NEmPE+EjQ==", + "dev": true, + "license": "MPL-2.0", + "peerDependencies": { + "eslint": "^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-security": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-3.0.1.tgz", + "integrity": "sha512-XjVGBhtDZJfyuhIxnQ/WMm385RbX3DBu7H1J7HNNhmB2tnGxMeqVSnYv79oAj992ayvIBZghsymwkYFS6cGH4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-regex": "^2.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint-plugin-vue": { "version": "10.10.0", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz", @@ -11187,6 +11215,16 @@ "node": ">= 0.10" } }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, "node_modules/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -11419,6 +11457,16 @@ "license": "MIT", "optional": true }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",