eslint-plugin-secure-coding 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  <a href="https://www.npmjs.com/package/eslint-plugin-secure-coding" target="_blank"><img src="https://img.shields.io/npm/v/eslint-plugin-secure-coding.svg" alt="NPM Version" /></a>
11
11
  <a href="https://www.npmjs.com/package/eslint-plugin-secure-coding" target="_blank"><img src="https://img.shields.io/npm/dm/eslint-plugin-secure-coding.svg" alt="NPM Downloads" /></a>
12
12
  <a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="Package License" /></a>
13
- <a href="https://app.codecov.io/gh/ofri-peretz/eslint/components?components%5B0%5D=secure-coding" target="_blank"><img src="https://codecov.io/gh/ofri-peretz/eslint/graph/badge.svg?component=secure-coding" alt="Codecov" /></a>
13
+ <a href="https://app.codecov.io/gh/ofri-peretz/eslint/components?components%5B0%5D=eslint-plugin-secure-coding" target="_blank"><img src="https://codecov.io/gh/ofri-peretz/eslint/graph/badge.svg?component=eslint-plugin-secure-coding" alt="Codecov" /></a>
14
14
  <a href="https://github.com/ofri-peretz/eslint" target="_blank"><img src="https://img.shields.io/badge/Since-Dec_2025-blue?logo=rocket&logoColor=white" alt="Since Dec 2025" /></a>
15
15
  </p>
16
16
 
@@ -37,6 +37,7 @@ npm install eslint-plugin-secure-coding --save-dev
37
37
  ```
38
38
 
39
39
  ## ⚙️ Configuration Presets
40
+
40
41
  | Preset | Description |
41
42
  | :-------------------- | :-------------------------------------------------------------- |
42
43
  | `recommended` | Balanced security for most projects (Web + key Mobile security) |
@@ -50,61 +51,61 @@ npm install eslint-plugin-secure-coding --save-dev
50
51
 
51
52
  **Legend**
52
53
 
53
- | Icon | Description |
54
- | :---: | :--- |
55
- | 💼 | **Recommended**: Included in the recommended preset. |
56
- | ⚠️ | **Warns**: Set towarn in recommended preset. |
57
- | 🔧 | **Auto-fixable**: Automatically fixable by the `--fix` CLI option. |
58
- | 💡 | **Suggestions**: Providing code suggestions in IDE. |
59
- | 🚫 | **Deprecated**: This rule is deprecated. |
60
-
61
- | Rule | CWE | OWASP | CVSS | Description | 💼 | ⚠️ | 🔧 | 💡 | 🚫 |
62
- | :--- | :---: | :---: | :---: | :--- | :---: | :---: | :---: | :---: | :---: |
63
- | [detect-non-literal-regexp](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/detect-non-literal-regexp) | CWE-400 | | 7.5 | ESLint security rule documentation for detect-non-literal-regexp | | ⚠️ | | | |
64
- | [detect-object-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/detect-object-injection) | CWE-915 | | 7.3 | ESLint security rule documentation for detect-object-injection | | ⚠️ | | | |
65
- | [detect-weak-password-validation](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/detect-weak-password-validation) | CWE-521 | | 7.5 | ESLint security rule documentation for detect-weak-password-validation | | | | | |
66
- | [no-directive-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-directive-injection) | CWE-94 | | 8.8 | ESLint security rule documentation for no-directive-injection | 💼 | | | | |
67
- | [no-electron-security-issues](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-electron-security-issues) | CWE-693 | | 8.8 | ESLint security rule documentation for no-electron-security-issues | 💼 | | | | |
68
- | [no-format-string-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-format-string-injection) | CWE-134 | | 9.8 | ESLint security rule documentation for no-format-string-injection | 💼 | | | | |
69
- | [no-graphql-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-graphql-injection) | CWE-943 | | 8.6 | ESLint security rule documentation for no-graphql-injection | 💼 | | | | |
70
- | [no-hardcoded-credentials](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-hardcoded-credentials) | CWE-798 | | 7.5 | ESLint security rule documentation for no-hardcoded-credentials | 💼 | | 🔧 | 💡 | |
71
- | [no-hardcoded-session-tokens](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-hardcoded-session-tokens) | CWE-798 | | 9.8 | ESLint security rule documentation for no-hardcoded-session-tokens | 💼 | | | | |
72
- | [no-improper-sanitization](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-improper-sanitization) | CWE-116 | | 7.5 | ESLint security rule documentation for no-improper-sanitization | 💼 | | | | |
73
- | [no-improper-type-validation](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-improper-type-validation) | CWE-20 | | 5.3 | ESLint security rule documentation for no-improper-type-validation | | ⚠️ | | | |
74
- | [no-insecure-comparison](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-insecure-comparison) | CWE-697 | | 5.3 | ESLint security rule documentation for no-insecure-comparison | | ⚠️ | 🔧 | | 🚫 |
75
- | [no-ldap-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-ldap-injection) | CWE-90 | | 9.8 | ESLint security rule documentation for no-ldap-injection | 💼 | | | | |
76
- | [no-missing-authentication](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-missing-authentication) | CWE-306 | | 9.8 | ESLint security rule documentation for no-missing-authentication | | ⚠️ | | | |
77
- | [no-pii-in-logs](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-pii-in-logs) | CWE-532 | | 7.5 | Enforce no pii in logs | | ⚠️ | | | |
78
- | [no-privilege-escalation](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-privilege-escalation) | CWE-269 | | 8.8 | ESLint security rule documentation for no-privilege-escalation | | ⚠️ | | | |
79
- | [no-redos-vulnerable-regex](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-redos-vulnerable-regex) | CWE-1333 | | 7.5 | ESLint security rule documentation for no-redos-vulnerable-regex | 💼 | | | 💡 | |
80
- | [no-sensitive-data-exposure](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-sensitive-data-exposure) | CWE-532 | | 5.5 | ESLint security rule documentation for no-sensitive-data-exposure | | ⚠️ | | 💡 | |
81
- | [no-unchecked-loop-condition](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-unchecked-loop-condition) | CWE-835 | | 7.5 | ESLint security rule documentation for no-unchecked-loop-condition | 💼 | | | | |
82
- | [no-unlimited-resource-allocation](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-unlimited-resource-allocation) | CWE-770 | | 7.5 | ESLint security rule documentation for no-unlimited-resource-allocation | 💼 | | | | |
83
- | [no-unsafe-deserialization](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-unsafe-deserialization) | CWE-502 | | 9.8 | ESLint security rule documentation for no-unsafe-deserialization | 💼 | | | | |
84
- | [no-unsafe-regex-construction](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-unsafe-regex-construction) | CWE-400 | | 7.5 | ESLint security rule documentation for no-unsafe-regex-construction | | ⚠️ | | 💡 | |
85
- | [no-weak-password-recovery](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-weak-password-recovery) | CWE-640 | | 9.8 | ESLint security rule documentation for no-weak-password-recovery | 💼 | | | | |
86
- | [no-xpath-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-xpath-injection) | CWE-643 | | 9.8 | ESLint security rule documentation for no-xpath-injection | 💼 | | | | |
87
- | [no-xxe-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-xxe-injection) | CWE-611 | | 9.1 | ESLint security rule documentation for no-xxe-injection | 💼 | | | | |
88
- | [require-backend-authorization](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/require-backend-authorization) | | | | ESLint security rule documentation for require-backend-authorization | | | | | |
89
- | [require-secure-defaults](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/require-secure-defaults) | CWE-276 | | 7.5 | ESLint security rule documentation for require-secure-defaults | | | | | |
54
+ | Icon | Description |
55
+ | :--: | :----------------------------------------------------------------- |
56
+ | 💼 | **Recommended**: Included in the recommended preset. |
57
+ | ⚠️ | **Warns**: Set towarn in recommended preset. |
58
+ | 🔧 | **Auto-fixable**: Automatically fixable by the `--fix` CLI option. |
59
+ | 💡 | **Suggestions**: Providing code suggestions in IDE. |
60
+ | 🚫 | **Deprecated**: This rule is deprecated. |
61
+
62
+ | Rule | CWE | OWASP | CVSS | Description | 💼 | ⚠️ | 🔧 | 💡 | 🚫 |
63
+ | :------------------------------------------------------------------------------------------------------------------------------------------- | :------: | :---: | :--: | :---------------------------------------------------------------------- | :-: | :-: | :-: | :-: | :-: |
64
+ | [detect-non-literal-regexp](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/detect-non-literal-regexp) | CWE-400 | | 7.5 | ESLint security rule documentation for detect-non-literal-regexp | | ⚠️ | | | |
65
+ | [detect-object-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/detect-object-injection) | CWE-915 | | 7.3 | ESLint security rule documentation for detect-object-injection | | ⚠️ | | | |
66
+ | [detect-weak-password-validation](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/detect-weak-password-validation) | CWE-521 | | 7.5 | ESLint security rule documentation for detect-weak-password-validation | | | | | |
67
+ | [no-directive-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-directive-injection) | CWE-94 | | 8.8 | ESLint security rule documentation for no-directive-injection | 💼 | | | | |
68
+ | [no-electron-security-issues](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-electron-security-issues) | CWE-693 | | 8.8 | ESLint security rule documentation for no-electron-security-issues | 💼 | | | | |
69
+ | [no-format-string-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-format-string-injection) | CWE-134 | | 9.8 | ESLint security rule documentation for no-format-string-injection | 💼 | | | | |
70
+ | [no-graphql-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-graphql-injection) | CWE-943 | | 8.6 | ESLint security rule documentation for no-graphql-injection | 💼 | | | | |
71
+ | [no-hardcoded-credentials](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-hardcoded-credentials) | CWE-798 | | 7.5 | ESLint security rule documentation for no-hardcoded-credentials | 💼 | | 🔧 | 💡 | |
72
+ | [no-hardcoded-session-tokens](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-hardcoded-session-tokens) | CWE-798 | | 9.8 | ESLint security rule documentation for no-hardcoded-session-tokens | 💼 | | | | |
73
+ | [no-improper-sanitization](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-improper-sanitization) | CWE-116 | | 7.5 | ESLint security rule documentation for no-improper-sanitization | 💼 | | | | |
74
+ | [no-improper-type-validation](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-improper-type-validation) | CWE-20 | | 5.3 | ESLint security rule documentation for no-improper-type-validation | | ⚠️ | | | |
75
+ | [no-insecure-comparison](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-insecure-comparison) | CWE-697 | | 5.3 | ESLint security rule documentation for no-insecure-comparison | | ⚠️ | 🔧 | | 🚫 |
76
+ | [no-ldap-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-ldap-injection) | CWE-90 | | 9.8 | ESLint security rule documentation for no-ldap-injection | 💼 | | | | |
77
+ | [no-missing-authentication](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-missing-authentication) | CWE-306 | | 9.8 | ESLint security rule documentation for no-missing-authentication | | ⚠️ | | | |
78
+ | [no-pii-in-logs](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-pii-in-logs) | CWE-532 | | 7.5 | Enforce no pii in logs | | ⚠️ | | | |
79
+ | [no-privilege-escalation](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-privilege-escalation) | CWE-269 | | 8.8 | ESLint security rule documentation for no-privilege-escalation | | ⚠️ | | | |
80
+ | [no-redos-vulnerable-regex](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-redos-vulnerable-regex) | CWE-1333 | | 7.5 | ESLint security rule documentation for no-redos-vulnerable-regex | 💼 | | | 💡 | |
81
+ | [no-sensitive-data-exposure](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-sensitive-data-exposure) | CWE-532 | | 5.5 | ESLint security rule documentation for no-sensitive-data-exposure | | ⚠️ | | 💡 | |
82
+ | [no-unchecked-loop-condition](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-unchecked-loop-condition) | CWE-835 | | 7.5 | ESLint security rule documentation for no-unchecked-loop-condition | 💼 | | | | |
83
+ | [no-unlimited-resource-allocation](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-unlimited-resource-allocation) | CWE-770 | | 7.5 | ESLint security rule documentation for no-unlimited-resource-allocation | 💼 | | | | |
84
+ | [no-unsafe-deserialization](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-unsafe-deserialization) | CWE-502 | | 9.8 | ESLint security rule documentation for no-unsafe-deserialization | 💼 | | | | |
85
+ | [no-unsafe-regex-construction](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-unsafe-regex-construction) | CWE-400 | | 7.5 | ESLint security rule documentation for no-unsafe-regex-construction | | ⚠️ | | 💡 | |
86
+ | [no-weak-password-recovery](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-weak-password-recovery) | CWE-640 | | 9.8 | ESLint security rule documentation for no-weak-password-recovery | 💼 | | | | |
87
+ | [no-xpath-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-xpath-injection) | CWE-643 | | 9.8 | ESLint security rule documentation for no-xpath-injection | 💼 | | | | |
88
+ | [no-xxe-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-xxe-injection) | CWE-611 | | 9.1 | ESLint security rule documentation for no-xxe-injection | 💼 | | | | |
89
+ | [require-backend-authorization](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/require-backend-authorization) | | | | ESLint security rule documentation for require-backend-authorization | | | | | |
90
+ | [require-secure-defaults](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/require-secure-defaults) | CWE-276 | | 7.5 | ESLint security rule documentation for require-secure-defaults | | | | | |
90
91
 
91
92
  ## 🔗 Related ESLint Plugins
92
93
 
93
94
  Part of the **Interlace ESLint Ecosystem** — AI-native security plugins with LLM-optimized error messages:
94
95
 
95
- | Plugin | Downloads | Description |
96
- | :--- | :---: | :--- |
97
- | [`eslint-plugin-secure-coding`](https://www.npmjs.com/package/eslint-plugin-secure-coding) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-secure-coding.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-secure-coding) | General security rules & OWASP guidelines. |
98
- | [`eslint-plugin-pg`](https://www.npmjs.com/package/eslint-plugin-pg) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-pg.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-pg) | PostgreSQL security & best practices. |
99
- | [`eslint-plugin-crypto`](https://www.npmjs.com/package/eslint-plugin-crypto) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-crypto.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-crypto) | NodeJS Cryptography security rules. |
100
- | [`eslint-plugin-jwt`](https://www.npmjs.com/package/eslint-plugin-jwt) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-jwt.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-jwt) | JWT security & best practices. |
101
- | [`eslint-plugin-browser-security`](https://www.npmjs.com/package/eslint-plugin-browser-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-browser-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-browser-security) | Browser-specific security & XSS prevention. |
102
- | [`eslint-plugin-express-security`](https://www.npmjs.com/package/eslint-plugin-express-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-express-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-express-security) | Express.js security hardening rules. |
103
- | [`eslint-plugin-lambda-security`](https://www.npmjs.com/package/eslint-plugin-lambda-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-lambda-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-lambda-security) | AWS Lambda security best practices. |
104
- | [`eslint-plugin-nestjs-security`](https://www.npmjs.com/package/eslint-plugin-nestjs-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-nestjs-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-nestjs-security) | NestJS security rules & patterns. |
105
- | [`eslint-plugin-mongodb-security`](https://www.npmjs.com/package/eslint-plugin-mongodb-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-mongodb-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-mongodb-security) | MongoDB security best practices. |
106
- | [`eslint-plugin-vercel-ai-security`](https://www.npmjs.com/package/eslint-plugin-vercel-ai-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-vercel-ai-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-vercel-ai-security) | Vercel AI SDK security hardening. |
107
- | [`eslint-plugin-import-next`](https://www.npmjs.com/package/eslint-plugin-import-next) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-import-next.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-import-next) | Next-gen import sorting & architecture. |
96
+ | Plugin | Downloads | Description |
97
+ | :--------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------ |
98
+ | [`eslint-plugin-secure-coding`](https://www.npmjs.com/package/eslint-plugin-secure-coding) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-secure-coding.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-secure-coding) | General security rules & OWASP guidelines. |
99
+ | [`eslint-plugin-pg`](https://www.npmjs.com/package/eslint-plugin-pg) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-pg.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-pg) | PostgreSQL security & best practices. |
100
+ | [`eslint-plugin-crypto`](https://www.npmjs.com/package/eslint-plugin-crypto) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-crypto.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-crypto) | NodeJS Cryptography security rules. |
101
+ | [`eslint-plugin-jwt`](https://www.npmjs.com/package/eslint-plugin-jwt) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-jwt.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-jwt) | JWT security & best practices. |
102
+ | [`eslint-plugin-browser-security`](https://www.npmjs.com/package/eslint-plugin-browser-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-browser-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-browser-security) | Browser-specific security & XSS prevention. |
103
+ | [`eslint-plugin-express-security`](https://www.npmjs.com/package/eslint-plugin-express-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-express-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-express-security) | Express.js security hardening rules. |
104
+ | [`eslint-plugin-lambda-security`](https://www.npmjs.com/package/eslint-plugin-lambda-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-lambda-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-lambda-security) | AWS Lambda security best practices. |
105
+ | [`eslint-plugin-nestjs-security`](https://www.npmjs.com/package/eslint-plugin-nestjs-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-nestjs-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-nestjs-security) | NestJS security rules & patterns. |
106
+ | [`eslint-plugin-mongodb-security`](https://www.npmjs.com/package/eslint-plugin-mongodb-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-mongodb-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-mongodb-security) | MongoDB security best practices. |
107
+ | [`eslint-plugin-vercel-ai-security`](https://www.npmjs.com/package/eslint-plugin-vercel-ai-security) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-vercel-ai-security.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-vercel-ai-security) | Vercel AI SDK security hardening. |
108
+ | [`eslint-plugin-import-next`](https://www.npmjs.com/package/eslint-plugin-import-next) | [![downloads](https://img.shields.io/npm/dt/eslint-plugin-import-next.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-import-next) | Next-gen import sorting & architecture. |
108
109
 
109
110
  ## 📄 License
110
111
 
@@ -112,4 +113,4 @@ MIT © [Ofri Peretz](https://github.com/ofri-peretz)
112
113
 
113
114
  <p align="center">
114
115
  <a href="https://eslint.interlace.tools/docs/security/plugin-secure-coding"><img src="https://eslint.interlace.tools/images/og-secure-coding.png" alt="ESLint Interlace Plugin" width="100%" /></a>
115
- </p>
116
+ </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-secure-coding",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "Security-focused ESLint plugin with 89 AI-parseable rules for detecting and preventing vulnerabilities. OWASP Top 10 2021 + Mobile Top 10 2024 coverage, CWE references, and AI-assisted fix guidance.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -378,6 +378,15 @@ exports.detectObjectInjection = (0, eslint_devkit_3.createRule)({
378
378
  if (propertyNode.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof propertyNode.value === 'number') {
379
379
  return false;
380
380
  }
381
+ // SAFE: Common numeric index variable names (i, j, k, index, idx, n)
382
+ // These are typically loop counters for array access
383
+ if (propertyNode.type === eslint_devkit_1.AST_NODE_TYPES.Identifier) {
384
+ const name = propertyNode.name;
385
+ const numericIndexNames = new Set(['i', 'j', 'k', 'index', 'idx', 'n', 'len']);
386
+ if (numericIndexNames.has(name)) {
387
+ return false;
388
+ }
389
+ }
381
390
  // Check if it's a literal string first
382
391
  if (isLiteralString(propertyNode)) {
383
392
  const propName = String(propertyNode.value);
@@ -402,6 +411,56 @@ exports.detectObjectInjection = (0, eslint_devkit_3.createRule)({
402
411
  // DANGEROUS: Any untyped/dynamic property access (e.g., obj[userInput])
403
412
  return true;
404
413
  };
414
+ /**
415
+ * Check if the object is a prototype-less object (Object.create(null))
416
+ * or is derived from an array spread/copy pattern
417
+ */
418
+ const isPrototypelessObject = (objectNode) => {
419
+ if (objectNode.type !== eslint_devkit_1.AST_NODE_TYPES.Identifier) {
420
+ return false;
421
+ }
422
+ const varName = objectNode.name;
423
+ // Walk up to find the variable declaration
424
+ let current = objectNode;
425
+ while (current) {
426
+ if (current.type === eslint_devkit_1.AST_NODE_TYPES.BlockStatement ||
427
+ current.type === eslint_devkit_1.AST_NODE_TYPES.Program) {
428
+ const statements = current.type === eslint_devkit_1.AST_NODE_TYPES.BlockStatement
429
+ ? current.body
430
+ : current.body;
431
+ for (const stmt of statements) {
432
+ if (stmt.type === eslint_devkit_1.AST_NODE_TYPES.VariableDeclaration) {
433
+ for (const decl of stmt.declarations) {
434
+ if (decl.id.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
435
+ decl.id.name === varName &&
436
+ decl.init) {
437
+ // Check for Object.create(null)
438
+ if (decl.init.type === eslint_devkit_1.AST_NODE_TYPES.CallExpression &&
439
+ decl.init.callee.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
440
+ decl.init.callee.object.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
441
+ decl.init.callee.object.name === 'Object' &&
442
+ decl.init.callee.property.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
443
+ decl.init.callee.property.name === 'create' &&
444
+ decl.init.arguments.length > 0 &&
445
+ decl.init.arguments[0].type === eslint_devkit_1.AST_NODE_TYPES.Literal &&
446
+ decl.init.arguments[0].value === null) {
447
+ return true;
448
+ }
449
+ // Check for array spread: [...array]
450
+ if (decl.init.type === eslint_devkit_1.AST_NODE_TYPES.ArrayExpression &&
451
+ decl.init.elements.length > 0 &&
452
+ decl.init.elements[0]?.type === eslint_devkit_1.AST_NODE_TYPES.SpreadElement) {
453
+ return true;
454
+ }
455
+ }
456
+ }
457
+ }
458
+ }
459
+ }
460
+ current = current.parent;
461
+ }
462
+ return false;
463
+ };
405
464
  /**
406
465
  * Extract property access information
407
466
  */
@@ -445,6 +504,10 @@ exports.detectObjectInjection = (0, eslint_devkit_3.createRule)({
445
504
  if (!node.left.computed) {
446
505
  return false;
447
506
  }
507
+ // SAFE: Object.create(null) objects have no prototype to pollute
508
+ if (isPrototypelessObject(node.left.object)) {
509
+ return false;
510
+ }
448
511
  const { propertyNode } = extractPropertyAccess(node);
449
512
  // Skip if the key has been validated (e.g., includes() or hasOwnProperty check)
450
513
  if (hasPrecedingValidation(propertyNode, node)) {
@@ -32,6 +32,13 @@ export interface Options extends SecurityRuleOptions {
32
32
  trustedGraphqlLibraries?: string[];
33
33
  /** Functions that validate GraphQL input */
34
34
  validationFunctions?: string[];
35
+ /**
36
+ * Callers where template literals should never be treated as GraphQL.
37
+ * Format: 'object.method' for member calls (e.g. 'console.log'),
38
+ * or 'ClassName' for constructors (e.g. 'URL', 'Error').
39
+ * These are merged with built-in safe callers.
40
+ */
41
+ safeTemplateLiteralCallers?: string[];
35
42
  }
36
43
  type RuleOptions = [Options?];
37
44
  export declare const noGraphqlInjection: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
@@ -136,6 +136,12 @@ exports.noGraphqlInjection = (0, eslint_devkit_1.createRule)({
136
136
  items: { type: 'string' },
137
137
  default: ['validate', 'sanitize', 'isValid', 'assertValid'],
138
138
  },
139
+ safeTemplateLiteralCallers: {
140
+ type: 'array',
141
+ items: { type: 'string' },
142
+ default: [],
143
+ description: 'Additional callers where template literals are never GraphQL. Format: object.method or ClassName.',
144
+ },
139
145
  trustedSanitizers: {
140
146
  type: 'array',
141
147
  items: { type: 'string' },
@@ -164,6 +170,7 @@ exports.noGraphqlInjection = (0, eslint_devkit_1.createRule)({
164
170
  maxQueryDepth: 10,
165
171
  trustedGraphqlLibraries: ['graphql', 'apollo-server', 'graphql-tools', 'graphql-tag'],
166
172
  validationFunctions: ['validate', 'sanitize', 'isValid', 'assertValid'],
173
+ safeTemplateLiteralCallers: [],
167
174
  trustedSanitizers: [],
168
175
  trustedAnnotations: [],
169
176
  strictMode: false,
@@ -171,7 +178,7 @@ exports.noGraphqlInjection = (0, eslint_devkit_1.createRule)({
171
178
  ],
172
179
  create(context) {
173
180
  const options = context.options[0] || {};
174
- const { allowIntrospection = false, maxQueryDepth = 10, trustedGraphqlLibraries = ['graphql', 'apollo-server', 'graphql-tools', 'graphql-tag'], validationFunctions = ['validate', 'sanitize', 'isValid', 'assertValid'], trustedSanitizers = [], trustedAnnotations = [], strictMode = false, } = options;
181
+ const { allowIntrospection = false, maxQueryDepth = 10, trustedGraphqlLibraries = ['graphql', 'apollo-server', 'graphql-tools', 'graphql-tag'], validationFunctions = ['validate', 'sanitize', 'isValid', 'assertValid'], safeTemplateLiteralCallers = [], trustedSanitizers = [], trustedAnnotations = [], strictMode = false, } = options;
175
182
  const sourceCode = context.sourceCode || context.sourceCode;
176
183
  const filename = context.filename || context.getFilename();
177
184
  // Create safety checker for false positive detection
@@ -197,46 +204,280 @@ exports.noGraphqlInjection = (0, eslint_devkit_1.createRule)({
197
204
  }
198
205
  return false;
199
206
  };
207
+ // ─── AST-based safe caller detection ───────────────────────────────
208
+ // Hard-coded safe callers that should NEVER contain GraphQL queries.
209
+ // Users can extend via safeTemplateLiteralCallers option.
210
+ const BUILTIN_SAFE_MEMBER_CALLERS = new Set([
211
+ 'console.log', 'console.warn', 'console.error', 'console.info', 'console.debug',
212
+ 'console.trace', 'logger.log', 'logger.info', 'logger.warn', 'logger.error',
213
+ 'logger.debug',
214
+ ]);
215
+ const BUILTIN_SAFE_CONSTRUCTORS = new Set(['URL', 'Error', 'TypeError', 'RangeError']);
216
+ // Merge user-provided callers
217
+ const safeMemberCallers = new Set(BUILTIN_SAFE_MEMBER_CALLERS);
218
+ const safeConstructors = new Set(BUILTIN_SAFE_CONSTRUCTORS);
219
+ for (const caller of safeTemplateLiteralCallers) {
220
+ if (caller.includes('.')) {
221
+ safeMemberCallers.add(caller);
222
+ }
223
+ else {
224
+ safeConstructors.add(caller);
225
+ }
226
+ }
200
227
  /**
201
- * Check if string contains GraphQL query patterns
228
+ * AST-based check: is this node inside a call that can't be GraphQL?
229
+ * Walks up from the TemplateLiteral to its nearest CallExpression/NewExpression parent.
202
230
  */
203
- const containsGraphqlQuery = (text) => {
204
- const lowerText = text.toLowerCase();
205
- return /\b(query|mutation|subscription|fragment)\b/.test(lowerText) ||
206
- /{\s*\w+/.test(lowerText) || // GraphQL selection sets
207
- /\btype\b|\binterface\b|\benum\b|\bscalar\b/.test(lowerText); // Schema definitions
231
+ const isInSafeCallerContext = (node) => {
232
+ let current = node.parent;
233
+ while (current) {
234
+ // Direct arg to a call: console.log(`...`)
235
+ if (current.type === 'CallExpression') {
236
+ const callee = current.callee;
237
+ // object.method() pattern
238
+ if (callee.type === 'MemberExpression' &&
239
+ callee.object.type === 'Identifier' &&
240
+ callee.property.type === 'Identifier') {
241
+ const key = `${callee.object.name}.${callee.property.name}`;
242
+ if (safeMemberCallers.has(key))
243
+ return true;
244
+ }
245
+ // Direct function call
246
+ if (callee.type === 'Identifier' && safeMemberCallers.has(callee.name)) {
247
+ return true;
248
+ }
249
+ break; // Stop at first enclosing call
250
+ }
251
+ // new URL(`...`), new Error(`...`)
252
+ if (current.type === eslint_devkit_1.AST_NODE_TYPES.NewExpression) {
253
+ if (current.callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
254
+ safeConstructors.has(current.callee.name)) {
255
+ return true;
256
+ }
257
+ break;
258
+ }
259
+ // Don't walk past function boundaries
260
+ if (current.type === eslint_devkit_1.AST_NODE_TYPES.ArrowFunctionExpression ||
261
+ current.type === eslint_devkit_1.AST_NODE_TYPES.FunctionExpression ||
262
+ current.type === eslint_devkit_1.AST_NODE_TYPES.FunctionDeclaration) {
263
+ break;
264
+ }
265
+ current = current.parent;
266
+ }
267
+ return false;
268
+ };
269
+ // ─── AST-based GraphQL detection helpers ──────────────────────────
270
+ const isWordChar = (ch) => {
271
+ const code = ch.charCodeAt(0);
272
+ return (code >= 65 && code <= 90) || // A-Z
273
+ (code >= 97 && code <= 122) || // a-z
274
+ (code >= 48 && code <= 57) || // 0-9
275
+ code === 95; // _
208
276
  };
277
+ const isWhitespace = (ch) => ch === ' ' || ch === '\n' || ch === '\t' || ch === '\r';
278
+ /** Operation keywords that must be followed by optional name + { or ( */
279
+ const GRAPHQL_OP_KEYWORDS = ['query', 'mutation', 'subscription'];
280
+ /** Schema keywords that must be followed by a type name */
281
+ const GRAPHQL_SCHEMA_KEYWORDS = ['type', 'interface', 'enum', 'scalar', 'input'];
209
282
  /**
210
- * Check if string contains dangerous introspection
283
+ * Find keyword at word boundary in text. Returns index or -1.
284
+ * Uses simple character checks instead of regex \b.
211
285
  */
212
- const containsIntrospection = (text) => {
213
- return /__schema|__type/i.test(text);
286
+ const findKeywordAtBoundary = (text, keyword, startFrom = 0) => {
287
+ let pos = startFrom;
288
+ while (pos < text.length) {
289
+ const idx = text.indexOf(keyword, pos);
290
+ if (idx === -1)
291
+ return -1;
292
+ const beforeOk = idx === 0 || !isWordChar(text[idx - 1]);
293
+ const afterIdx = idx + keyword.length;
294
+ const afterOk = afterIdx >= text.length || !isWordChar(text[afterIdx]);
295
+ if (beforeOk && afterOk)
296
+ return idx;
297
+ pos = idx + 1;
298
+ }
299
+ return -1;
214
300
  };
215
301
  /**
216
- * Calculate query depth (rough estimate)
302
+ * AST-based: Check if a TemplateLiteral contains GraphQL syntax.
303
+ * Examines quasis (static template parts) directly — no regex, no sourceCode.getText().
217
304
  */
218
- const calculateQueryDepth = (queryText) => {
305
+ const isGraphqlTemplate = (node) => {
306
+ // Build combined static text from quasis for keyword scanning
307
+ const staticText = node.quasis
308
+ .map(q => (q.value.cooked ?? q.value.raw).toLowerCase())
309
+ .join('');
310
+ // 1. Check for GraphQL operation keywords (query, mutation, subscription)
311
+ // Must be followed by optional name then { or (
312
+ for (const keyword of GRAPHQL_OP_KEYWORDS) {
313
+ let pos = 0;
314
+ while (pos < staticText.length) {
315
+ const idx = findKeywordAtBoundary(staticText, keyword, pos);
316
+ if (idx === -1)
317
+ break;
318
+ pos = idx + 1;
319
+ // Scan past keyword, skip whitespace, skip optional name, skip whitespace, expect { or (
320
+ let scan = idx + keyword.length;
321
+ while (scan < staticText.length && isWhitespace(staticText[scan]))
322
+ scan++;
323
+ while (scan < staticText.length && isWordChar(staticText[scan]))
324
+ scan++;
325
+ while (scan < staticText.length && isWhitespace(staticText[scan]))
326
+ scan++;
327
+ if (scan < staticText.length && (staticText[scan] === '{' || staticText[scan] === '(')) {
328
+ return true;
329
+ }
330
+ }
331
+ }
332
+ // Check for fragment keyword: fragment Name on Type
333
+ {
334
+ let pos = 0;
335
+ while (pos < staticText.length) {
336
+ const idx = findKeywordAtBoundary(staticText, 'fragment', pos);
337
+ if (idx === -1)
338
+ break;
339
+ pos = idx + 1;
340
+ let scan = idx + 8; // 'fragment'.length
341
+ while (scan < staticText.length && isWhitespace(staticText[scan]))
342
+ scan++;
343
+ const nameStart = scan;
344
+ while (scan < staticText.length && isWordChar(staticText[scan]))
345
+ scan++;
346
+ if (scan === nameStart)
347
+ continue; // no name
348
+ while (scan < staticText.length && isWhitespace(staticText[scan]))
349
+ scan++;
350
+ if (staticText.slice(scan, scan + 2) === 'on' && (scan + 2 >= staticText.length || !isWordChar(staticText[scan + 2]))) {
351
+ return true;
352
+ }
353
+ }
354
+ }
355
+ // 2. Check for schema definition keywords (type User, interface Foo, etc.)
356
+ for (const keyword of GRAPHQL_SCHEMA_KEYWORDS) {
357
+ let pos = 0;
358
+ while (pos < staticText.length) {
359
+ const idx = findKeywordAtBoundary(staticText, keyword, pos);
360
+ if (idx === -1)
361
+ break;
362
+ pos = idx + 1;
363
+ let scan = idx + keyword.length;
364
+ // Must have whitespace then a word (type name)
365
+ if (scan >= staticText.length || !isWhitespace(staticText[scan]))
366
+ continue;
367
+ while (scan < staticText.length && isWhitespace(staticText[scan]))
368
+ scan++;
369
+ if (scan < staticText.length && isWordChar(staticText[scan])) {
370
+ return true;
371
+ }
372
+ }
373
+ }
374
+ // 3. Check for selection sets (nested braces): { users { name } }
375
+ let braceDepth = 0;
376
+ for (let i = 0; i < staticText.length; i++) {
377
+ if (staticText[i] === '{') {
378
+ braceDepth++;
379
+ if (braceDepth >= 2)
380
+ return true;
381
+ }
382
+ else if (staticText[i] === '}') {
383
+ braceDepth = Math.max(0, braceDepth - 1);
384
+ }
385
+ }
386
+ return false;
387
+ };
388
+ /**
389
+ * Check if a TemplateLiteral contains introspection patterns.
390
+ * AST-based: scans quasis directly.
391
+ */
392
+ const templateHasIntrospection = (node) => {
393
+ return node.quasis.some(q => {
394
+ const text = (q.value.cooked ?? q.value.raw).toLowerCase();
395
+ return text.includes('__schema') || text.includes('__type');
396
+ });
397
+ };
398
+ /**
399
+ * Calculate query depth from template quasis (brace depth scan).
400
+ */
401
+ const templateQueryDepth = (node) => {
219
402
  let depth = 0;
220
403
  let braceCount = 0;
221
- for (const char of queryText) {
222
- if (char === '{') {
223
- braceCount++;
224
- depth = Math.max(depth, braceCount);
225
- }
226
- else if (char === '}') {
227
- braceCount--;
404
+ for (const q of node.quasis) {
405
+ const text = q.value.cooked ?? q.value.raw;
406
+ for (const char of text) {
407
+ if (char === '{') {
408
+ braceCount++;
409
+ depth = Math.max(depth, braceCount);
410
+ }
411
+ else if (char === '}') {
412
+ braceCount--;
413
+ }
228
414
  }
229
415
  }
230
416
  return depth;
231
417
  };
232
418
  /**
233
- * Check if string contains unsafe interpolation
419
+ * Text-based GraphQL detection for string Literals and BinaryExpressions.
420
+ * Uses simple string methods — only regex is for fragment pattern.
234
421
  */
235
- const containsUnsafeInterpolation = (text) => {
236
- // Check for template literal interpolation
237
- return /\$\{[^}]+\}/.test(text) ||
238
- // Check for string concatenation patterns
239
- /\w+\s*\+\s*[^+]+\s*\+\s*\w+/.test(text);
422
+ const containsGraphqlText = (text) => {
423
+ const lower = text.toLowerCase();
424
+ // Check operation keywords
425
+ for (const keyword of GRAPHQL_OP_KEYWORDS) {
426
+ const idx = findKeywordAtBoundary(lower, keyword);
427
+ if (idx === -1)
428
+ continue;
429
+ let scan = idx + keyword.length;
430
+ while (scan < lower.length && isWhitespace(lower[scan]))
431
+ scan++;
432
+ while (scan < lower.length && isWordChar(lower[scan]))
433
+ scan++;
434
+ while (scan < lower.length && isWhitespace(lower[scan]))
435
+ scan++;
436
+ if (scan < lower.length && (lower[scan] === '{' || lower[scan] === '('))
437
+ return true;
438
+ }
439
+ // Check fragment
440
+ const fragIdx = findKeywordAtBoundary(lower, 'fragment');
441
+ if (fragIdx !== -1) {
442
+ let scan = fragIdx + 8;
443
+ while (scan < lower.length && isWhitespace(lower[scan]))
444
+ scan++;
445
+ const nameStart = scan;
446
+ while (scan < lower.length && isWordChar(lower[scan]))
447
+ scan++;
448
+ if (scan > nameStart) {
449
+ while (scan < lower.length && isWhitespace(lower[scan]))
450
+ scan++;
451
+ if (lower.slice(scan, scan + 2) === 'on' && (scan + 2 >= lower.length || !isWordChar(lower[scan + 2])))
452
+ return true;
453
+ }
454
+ }
455
+ // Check schema keywords
456
+ for (const keyword of GRAPHQL_SCHEMA_KEYWORDS) {
457
+ const idx = findKeywordAtBoundary(lower, keyword);
458
+ if (idx === -1)
459
+ continue;
460
+ let scan = idx + keyword.length;
461
+ if (scan >= lower.length || !isWhitespace(lower[scan]))
462
+ continue;
463
+ while (scan < lower.length && isWhitespace(lower[scan]))
464
+ scan++;
465
+ if (scan < lower.length && isWordChar(lower[scan]))
466
+ return true;
467
+ }
468
+ // Check nested braces
469
+ let braceDepth = 0;
470
+ for (let i = 0; i < text.length; i++) {
471
+ if (text[i] === '{') {
472
+ braceDepth++;
473
+ if (braceDepth >= 2)
474
+ return true;
475
+ }
476
+ else if (text[i] === '}') {
477
+ braceDepth = Math.max(0, braceDepth - 1);
478
+ }
479
+ }
480
+ return false;
240
481
  };
241
482
  /**
242
483
  * Check if input is validated before use
@@ -258,12 +499,16 @@ exports.noGraphqlInjection = (0, eslint_devkit_1.createRule)({
258
499
  return {
259
500
  // Check template literals for GraphQL queries
260
501
  TemplateLiteral(node) {
261
- const queryText = sourceCode.getText(node);
262
- if (!containsGraphqlQuery(queryText)) {
502
+ // AST-based context check: skip templates inside safe callers
503
+ if (isInSafeCallerContext(node)) {
263
504
  return;
264
505
  }
265
- // Check for introspection queries
266
- if (!allowIntrospection && containsIntrospection(queryText)) {
506
+ // AST-based GraphQL detection: scan quasis, no regex
507
+ if (!isGraphqlTemplate(node)) {
508
+ return;
509
+ }
510
+ // Check for introspection queries (AST-based)
511
+ if (!allowIntrospection && templateHasIntrospection(node)) {
267
512
  // FALSE POSITIVE REDUCTION: Skip if annotated as safe
268
513
  if (safetyChecker.isSafe(node, context)) {
269
514
  return;
@@ -278,8 +523,8 @@ exports.noGraphqlInjection = (0, eslint_devkit_1.createRule)({
278
523
  });
279
524
  return;
280
525
  }
281
- // Check for unsafe interpolation
282
- if (containsUnsafeInterpolation(queryText)) {
526
+ // Check for unsafe interpolation (AST-based: just check expressions array)
527
+ if (node.expressions.length > 0) {
283
528
  // FALSE POSITIVE REDUCTION: Skip if all expressions are validated
284
529
  const allExpressionsSafe = node.expressions.every((expr) => isInputValidated(expr) || safetyChecker.isSafe(expr, context));
285
530
  if (!allExpressionsSafe) {
@@ -299,8 +544,8 @@ exports.noGraphqlInjection = (0, eslint_devkit_1.createRule)({
299
544
  });
300
545
  }
301
546
  }
302
- // Check query depth for DoS protection
303
- const depth = calculateQueryDepth(queryText);
547
+ // Check query depth for DoS protection (AST-based)
548
+ const depth = templateQueryDepth(node);
304
549
  if (depth > maxQueryDepth) {
305
550
  context.report({
306
551
  node,
@@ -324,11 +569,12 @@ exports.noGraphqlInjection = (0, eslint_devkit_1.createRule)({
324
569
  return;
325
570
  }
326
571
  const queryText = node.value;
327
- if (!containsGraphqlQuery(queryText)) {
572
+ if (!containsGraphqlText(queryText)) {
328
573
  return;
329
574
  }
330
575
  // Check for introspection queries
331
- if (!allowIntrospection && containsIntrospection(queryText)) {
576
+ const lowerQuery = queryText.toLowerCase();
577
+ if (!allowIntrospection && (lowerQuery.includes('__schema') || lowerQuery.includes('__type'))) {
332
578
  /* c8 ignore start -- safetyChecker requires JSDoc annotations not testable via RuleTester */
333
579
  if (safetyChecker.isSafe(node, context)) {
334
580
  return;
@@ -343,8 +589,18 @@ exports.noGraphqlInjection = (0, eslint_devkit_1.createRule)({
343
589
  },
344
590
  });
345
591
  }
346
- // Check query depth
347
- const depth = calculateQueryDepth(queryText);
592
+ // Check query depth via brace scanner
593
+ let depth = 0;
594
+ let bc = 0;
595
+ for (const ch of queryText) {
596
+ if (ch === '{') {
597
+ bc++;
598
+ depth = Math.max(depth, bc);
599
+ }
600
+ else if (ch === '}') {
601
+ bc--;
602
+ }
603
+ }
348
604
  if (depth > maxQueryDepth) {
349
605
  context.report({
350
606
  node,
@@ -362,7 +618,7 @@ exports.noGraphqlInjection = (0, eslint_devkit_1.createRule)({
362
618
  return;
363
619
  }
364
620
  const fullText = sourceCode.getText(node);
365
- if (!containsGraphqlQuery(fullText)) {
621
+ if (!containsGraphqlText(fullText)) {
366
622
  return;
367
623
  }
368
624
  // String concatenation in GraphQL queries is dangerous
@@ -135,6 +135,15 @@ exports.noInsecureComparison = (0, eslint_devkit_2.createRule)({
135
135
  // Timing-safe comparison for secrets even with strict equality
136
136
  if ((node.operator === '===' || node.operator === '!==') &&
137
137
  (isPotentialSecret(node.left) || isPotentialSecret(node.right))) {
138
+ // SKIP: Length comparisons are safe - they're actually required before timingSafeEqual
139
+ const isLengthComparison = (expr) => {
140
+ return expr.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
141
+ expr.property.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
142
+ expr.property.name === 'length';
143
+ };
144
+ if (isLengthComparison(node.left) || isLengthComparison(node.right)) {
145
+ return; // Length checks are safe and recommended
146
+ }
138
147
  const leftText = sourceCode.getText(node.left);
139
148
  const rightText = sourceCode.getText(node.right);
140
149
  // ... rest of logic uses example ...
@@ -9,11 +9,23 @@ exports.noSensitiveDataExposure = void 0;
9
9
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
10
  const eslint_devkit_2 = require("@interlace/eslint-devkit");
11
11
  /**
12
- * Check if string contains sensitive data patterns
12
+ * Check if string contains sensitive data patterns.
13
+ * Handles camelCase (secretKey), snake_case (secret_key), and plain text.
13
14
  */
14
15
  function containsSensitiveData(text, patterns) {
15
- const lowerText = text.toLowerCase();
16
- return patterns.some(pattern => lowerText.includes(pattern.toLowerCase()));
16
+ // Normalize camelCase → space separated for matching (secretKey → secret key)
17
+ const normalized = text
18
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
19
+ .toLowerCase();
20
+ for (const pattern of patterns) {
21
+ const escaped = pattern.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22
+ // Allow spaces or underscores as word separators (e.g. 'credit card' matches 'credit_card')
23
+ const flexPattern = escaped.replace(/[_ ]/g, '[_ ]');
24
+ if (new RegExp(`\\b${flexPattern}\\b`, 'i').test(normalized)) {
25
+ return pattern;
26
+ }
27
+ }
28
+ return null;
17
29
  }
18
30
  exports.noSensitiveDataExposure = (0, eslint_devkit_2.createRule)({
19
31
  name: 'no-sensitive-data-exposure',
@@ -65,7 +77,7 @@ exports.noSensitiveDataExposure = (0, eslint_devkit_2.createRule)({
65
77
  sensitivePatterns: {
66
78
  type: 'array',
67
79
  items: { type: 'string' },
68
- default: ['password', 'secret', 'token', 'key', 'ssn', 'credit', 'card', 'api_key', 'apikey'],
80
+ default: ['password', 'passwd', 'secret', 'token', 'access_token', 'auth_token', 'ssn', 'credit_card', 'creditcard', 'api_key', 'apikey', 'secret_key', 'private_key', 'encryption_key'],
69
81
  description: 'Sensitive data patterns',
70
82
  },
71
83
  checkConsoleLog: {
@@ -90,14 +102,14 @@ exports.noSensitiveDataExposure = (0, eslint_devkit_2.createRule)({
90
102
  },
91
103
  defaultOptions: [
92
104
  {
93
- sensitivePatterns: ['password', 'secret', 'token', 'key', 'ssn', 'credit', 'card', 'api_key', 'apikey'],
105
+ sensitivePatterns: ['password', 'passwd', 'secret', 'token', 'access_token', 'auth_token', 'ssn', 'credit_card', 'creditcard', 'api_key', 'apikey', 'secret_key', 'private_key', 'encryption_key'],
94
106
  checkConsoleLog: true,
95
107
  checkErrorMessages: true,
96
108
  checkApiResponses: true,
97
109
  },
98
110
  ],
99
111
  create(context, [options = {}]) {
100
- const { sensitivePatterns = ['password', 'secret', 'token', 'key', 'ssn', 'credit', 'card', 'api_key', 'apikey'], checkConsoleLog = true, checkErrorMessages = true, } = options || {};
112
+ const { sensitivePatterns = ['password', 'passwd', 'secret', 'token', 'access_token', 'auth_token', 'ssn', 'credit_card', 'creditcard', 'api_key', 'apikey', 'secret_key', 'private_key', 'encryption_key'], checkConsoleLog = true, checkErrorMessages = true, } = options || {};
101
113
  /**
102
114
  * Check CallExpression for logging calls with sensitive data
103
115
  */
@@ -134,13 +146,14 @@ exports.noSensitiveDataExposure = (0, eslint_devkit_2.createRule)({
134
146
  for (const arg of node.arguments) {
135
147
  if (arg.type === 'Literal' && typeof arg.value === 'string') {
136
148
  const text = arg.value;
137
- if (containsSensitiveData(text, sensitivePatterns)) {
149
+ const matchedPattern = containsSensitiveData(text, sensitivePatterns);
150
+ if (matchedPattern) {
138
151
  context.report({
139
152
  node: arg,
140
153
  messageId: 'sensitiveDataExposure',
141
154
  data: {
142
155
  context: 'logs',
143
- dataType: 'password',
156
+ dataType: matchedPattern,
144
157
  },
145
158
  suggest: [
146
159
  { messageId: 'redactData', fix: () => null },
@@ -152,14 +165,14 @@ exports.noSensitiveDataExposure = (0, eslint_devkit_2.createRule)({
152
165
  }
153
166
  }
154
167
  else if (arg.type === 'Identifier' && arg.name) {
155
- const name = arg.name.toLowerCase();
156
- if (containsSensitiveData(name, sensitivePatterns)) {
168
+ const matchedPattern2 = containsSensitiveData(arg.name, sensitivePatterns);
169
+ if (matchedPattern2) {
157
170
  context.report({
158
171
  node: arg,
159
172
  messageId: 'sensitiveDataExposure',
160
173
  data: {
161
174
  context: 'logs',
162
- dataType: 'password',
175
+ dataType: matchedPattern2,
163
176
  },
164
177
  suggest: [
165
178
  { messageId: 'redactData', fix: () => null },
@@ -185,13 +198,14 @@ exports.noSensitiveDataExposure = (0, eslint_devkit_2.createRule)({
185
198
  for (const arg of node.arguments) {
186
199
  if (arg.type === 'Literal' && typeof arg.value === 'string') {
187
200
  const text = arg.value;
188
- if (containsSensitiveData(text, sensitivePatterns)) {
201
+ const matchedErrPattern = containsSensitiveData(text, sensitivePatterns);
202
+ if (matchedErrPattern) {
189
203
  context.report({
190
204
  node: arg,
191
205
  messageId: 'sensitiveDataExposure',
192
206
  data: {
193
207
  context: 'error messages',
194
- dataType: 'password',
208
+ dataType: matchedErrPattern,
195
209
  },
196
210
  suggest: [
197
211
  { messageId: 'redactData', fix: () => null },
@@ -206,13 +220,14 @@ exports.noSensitiveDataExposure = (0, eslint_devkit_2.createRule)({
206
220
  // Check left side if it's a literal
207
221
  if (arg.left && arg.left.type === 'Literal' && typeof arg.left.value === 'string') {
208
222
  const leftText = arg.left.value;
209
- if (containsSensitiveData(leftText, sensitivePatterns)) {
223
+ const leftMatchedPattern = containsSensitiveData(leftText, sensitivePatterns);
224
+ if (leftMatchedPattern) {
210
225
  context.report({
211
226
  node: arg.left,
212
227
  messageId: 'sensitiveDataExposure',
213
228
  data: {
214
229
  context: 'error messages',
215
- dataType: 'password',
230
+ dataType: leftMatchedPattern,
216
231
  },
217
232
  suggest: [
218
233
  { messageId: 'redactData', fix: () => null },
@@ -225,14 +240,14 @@ exports.noSensitiveDataExposure = (0, eslint_devkit_2.createRule)({
225
240
  }
226
241
  // Check right side if it's an identifier
227
242
  if (arg.right && arg.right.type === 'Identifier' && arg.right.name) {
228
- const rightName = arg.right.name.toLowerCase();
229
- if (containsSensitiveData(rightName, sensitivePatterns)) {
243
+ const rightMatchedPattern = containsSensitiveData(arg.right.name, sensitivePatterns);
244
+ if (rightMatchedPattern) {
230
245
  context.report({
231
246
  node: arg.right,
232
247
  messageId: 'sensitiveDataExposure',
233
248
  data: {
234
249
  context: 'error messages',
235
- dataType: 'password',
250
+ dataType: rightMatchedPattern,
236
251
  },
237
252
  suggest: [
238
253
  { messageId: 'redactData', fix: () => null },
@@ -355,8 +355,32 @@ exports.noXpathInjection = (0, eslint_devkit_1.createRule)({
355
355
  // Check template literals for XPath expressions
356
356
  TemplateLiteral(node) {
357
357
  const fullText = sourceCode.getText(node);
358
- // Check if this looks like an XPath expression
359
- if (!fullText.includes('/') && !fullText.includes('[') && !fullText.includes('@')) {
358
+ // Skip common non-XPath patterns
359
+ // URLs and API endpoints
360
+ if (/https?:\/\//.test(fullText) || /^[`'"]\s*\/api\//.test(fullText)) {
361
+ return;
362
+ }
363
+ // File paths (start with / or contain common path patterns)
364
+ if (/^[`'"]\s*\/home\//.test(fullText) || /^[`'"]\s*\/usr\//.test(fullText) || /^[`'"]\s*\/tmp\//.test(fullText)) {
365
+ return;
366
+ }
367
+ // CSS selectors
368
+ if (/\[data-[\w-]+/.test(fullText) || /\[class=/.test(fullText) || /\[id=/.test(fullText)) {
369
+ return;
370
+ }
371
+ // Search/query strings
372
+ if (/\?.*=/.test(fullText) && !/\[@/.test(fullText)) {
373
+ return;
374
+ }
375
+ // Check if this looks like an ACTUAL XPath expression
376
+ // Must have XPath-specific syntax, not just forward slashes
377
+ const hasXpathSyntax = /\/\/\w+/.test(fullText) || // //element
378
+ /\[@\w+/.test(fullText) || // [@attr
379
+ /\[contains\(/.test(fullText) || // [contains(
380
+ /\[text\(\)/.test(fullText) || // [text()
381
+ /\/child::/.test(fullText) || // /child::
382
+ /\/descendant::/.test(fullText); // /descendant::
383
+ if (!hasXpathSyntax) {
360
384
  return;
361
385
  }
362
386
  // Check for interpolation in XPath-like expressions