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-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", diff --git a/package.json b/package.json index cdab0b9afca..622e1ffc64d 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",