feat: add eslint-plugin-security rules for static analysis

Adds 12 security-focused ESLint rules (unsafe-regex, eval, timing
attacks, child-process, bidi-characters, etc). One legitimate
non-literal RegExp in answers.ts suppressed with inline comment.
CI already runs pnpm lint + pnpm audit for SAST coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 05:26:10 +00:00
co-authored by Claude Opus 4.6
parent abc081487c
commit e2dc2bbd70
4 changed files with 41 additions and 0 deletions
+15
View File
@@ -1,5 +1,6 @@
import tseslint from '@typescript-eslint/eslint-plugin'
import tsparser from '@typescript-eslint/parser'
import security from 'eslint-plugin-security'
export default [
{
@@ -15,10 +16,24 @@ export default [
},
plugins: {
'@typescript-eslint': tseslint,
security: security,
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
// Security rules (from eslint-plugin-security)
'security/detect-buffer-noassert': 'warn',
'security/detect-child-process': 'warn',
'security/detect-eval-with-expression': 'error',
'security/detect-new-buffer': 'warn',
'security/detect-non-literal-regexp': 'warn',
'security/detect-non-literal-require': 'warn',
'security/detect-possible-timing-attacks': 'warn',
'security/detect-pseudoRandomBytes': 'warn',
'security/detect-unsafe-regex': 'error',
'security/detect-bidi-characters': 'error',
// detect-object-injection has too many false positives — skip
// detect-non-literal-fs-filename too noisy for server code — skip
},
},
// Frontend game engine: fire-and-forget async (audio, animations) is intentional
+1
View File
@@ -19,6 +19,7 @@
"@typescript-eslint/parser": "^8.56.1",
"concurrently": "^9.1.2",
"eslint": "^10.0.3",
"eslint-plugin-security": "^4.0.0",
"typescript": "^5.7.3",
"vitest": "^3.1.1"
}
+24
View File
@@ -27,6 +27,9 @@ importers:
eslint:
specifier: ^10.0.3
version: 10.0.3(jiti@2.6.1)
eslint-plugin-security:
specifier: ^4.0.0
version: 4.0.0
typescript:
specifier: ^5.7.3
version: 5.9.3
@@ -2162,6 +2165,10 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
eslint-plugin-security@4.0.0:
resolution: {integrity: sha512-tfuQT8K/Li1ZxhFzyD8wPIKtlzZxqBcPr9q0jFMQ77wWAbKBVEhaMPVQRTMTvCMUDhwBe5vPVqQPwAGk/ASfxQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-scope@9.1.2:
resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
@@ -3051,6 +3058,10 @@ packages:
regenerate@1.4.2:
resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
regexp-tree@0.1.27:
resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
hasBin: true
regexp.prototype.flags@1.5.4:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
@@ -3114,6 +3125,9 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'}
safe-regex@2.1.1:
resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==}
saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
@@ -5791,6 +5805,10 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-plugin-security@4.0.0:
dependencies:
safe-regex: 2.1.1
eslint-scope@9.1.2:
dependencies:
'@types/esrecurse': 4.3.1
@@ -6681,6 +6699,8 @@ snapshots:
regenerate@1.4.2: {}
regexp-tree@0.1.27: {}
regexp.prototype.flags@1.5.4:
dependencies:
call-bind: 1.0.8
@@ -6786,6 +6806,10 @@ snapshots:
es-errors: 1.3.0
is-regex: 1.2.1
safe-regex@2.1.1:
dependencies:
regexp-tree: 0.1.27
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
+1
View File
@@ -155,6 +155,7 @@ export function checkAnswer(response: string | null, acceptedAnswers: string[]):
if (accNum !== null) {
const numStr = String(accNum)
// Only match if the number appears as a whole token, not as a substring of a larger number
// eslint-disable-next-line security/detect-non-literal-regexp -- numStr is a parsed number, escaped
const numRegex = new RegExp(`(?<![\\d])${numStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\d])`)
if (numRegex.test(normResponse)) return 1.0
// Check number words in response