diff --git a/eslint.config.js b/eslint.config.js index 7984fb7..0582543 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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 diff --git a/package.json b/package.json index dcfc5cf..22db46f 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fe0c9c..ff6fca3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/server/src/engine/answers.ts b/server/src/engine/answers.ts index e86fad0..6c331aa 100644 --- a/server/src/engine/answers.ts +++ b/server/src/engine/answers.ts @@ -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(`(?