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 +53 -52
- package/package.json +1 -1
- package/src/rules/detect-object-injection/index.js +63 -0
- package/src/rules/no-graphql-injection/index.d.ts +7 -0
- package/src/rules/no-graphql-injection/index.js +294 -38
- package/src/rules/no-insecure-comparison/index.js +9 -0
- package/src/rules/no-sensitive-data-exposure/index.js +33 -18
- package/src/rules/no-xpath-injection/index.js +26 -2
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
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
61
|
-
| Rule
|
|
62
|
-
|
|
|
63
|
-
| [detect-non-literal-regexp](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/detect-non-literal-regexp)
|
|
64
|
-
| [detect-object-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/detect-object-injection)
|
|
65
|
-
| [detect-weak-password-validation](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/detect-weak-password-validation)
|
|
66
|
-
| [no-directive-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-directive-injection)
|
|
67
|
-
| [no-electron-security-issues](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-electron-security-issues)
|
|
68
|
-
| [no-format-string-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-format-string-injection)
|
|
69
|
-
| [no-graphql-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-graphql-injection)
|
|
70
|
-
| [no-hardcoded-credentials](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-hardcoded-credentials)
|
|
71
|
-
| [no-hardcoded-session-tokens](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-hardcoded-session-tokens)
|
|
72
|
-
| [no-improper-sanitization](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-improper-sanitization)
|
|
73
|
-
| [no-improper-type-validation](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-improper-type-validation)
|
|
74
|
-
| [no-insecure-comparison](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-insecure-comparison)
|
|
75
|
-
| [no-ldap-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-ldap-injection)
|
|
76
|
-
| [no-missing-authentication](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-missing-authentication)
|
|
77
|
-
| [no-pii-in-logs](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-pii-in-logs)
|
|
78
|
-
| [no-privilege-escalation](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-privilege-escalation)
|
|
79
|
-
| [no-redos-vulnerable-regex](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-redos-vulnerable-regex)
|
|
80
|
-
| [no-sensitive-data-exposure](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-sensitive-data-exposure)
|
|
81
|
-
| [no-unchecked-loop-condition](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/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
|
|
83
|
-
| [no-unsafe-deserialization](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-unsafe-deserialization)
|
|
84
|
-
| [no-unsafe-regex-construction](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-unsafe-regex-construction)
|
|
85
|
-
| [no-weak-password-recovery](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-weak-password-recovery)
|
|
86
|
-
| [no-xpath-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-xpath-injection)
|
|
87
|
-
| [no-xxe-injection](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/no-xxe-injection)
|
|
88
|
-
| [require-backend-authorization](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/require-backend-authorization)
|
|
89
|
-
| [require-secure-defaults](https://eslint.interlace.tools/docs/security/plugin-secure-coding/rules/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
|
|
96
|
-
|
|
|
97
|
-
| [`eslint-plugin-secure-coding`](https://www.npmjs.com/package/eslint-plugin-secure-coding)
|
|
98
|
-
| [`eslint-plugin-pg`](https://www.npmjs.com/package/eslint-plugin-pg)
|
|
99
|
-
| [`eslint-plugin-crypto`](https://www.npmjs.com/package/eslint-plugin-crypto)
|
|
100
|
-
| [`eslint-plugin-jwt`](https://www.npmjs.com/package/eslint-plugin-jwt)
|
|
101
|
-
| [`eslint-plugin-browser-security`](https://www.npmjs.com/package/eslint-plugin-browser-security)
|
|
102
|
-
| [`eslint-plugin-express-security`](https://www.npmjs.com/package/eslint-plugin-express-security)
|
|
103
|
-
| [`eslint-plugin-lambda-security`](https://www.npmjs.com/package/eslint-plugin-lambda-security)
|
|
104
|
-
| [`eslint-plugin-nestjs-security`](https://www.npmjs.com/package/eslint-plugin-nestjs-security)
|
|
105
|
-
| [`eslint-plugin-mongodb-security`](https://www.npmjs.com/package/eslint-plugin-mongodb-security)
|
|
106
|
-
| [`eslint-plugin-vercel-ai-security`](https://www.npmjs.com/package/eslint-plugin-vercel-ai-security) | [](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)
|
|
96
|
+
| Plugin | Downloads | Description |
|
|
97
|
+
| :--------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------ |
|
|
98
|
+
| [`eslint-plugin-secure-coding`](https://www.npmjs.com/package/eslint-plugin-secure-coding) | [](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) | [](https://www.npmjs.com/package/eslint-plugin-pg) | PostgreSQL security & best practices. |
|
|
100
|
+
| [`eslint-plugin-crypto`](https://www.npmjs.com/package/eslint-plugin-crypto) | [](https://www.npmjs.com/package/eslint-plugin-crypto) | NodeJS Cryptography security rules. |
|
|
101
|
+
| [`eslint-plugin-jwt`](https://www.npmjs.com/package/eslint-plugin-jwt) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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) | [](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.
|
|
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
|
-
*
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
*
|
|
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
|
|
213
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
*
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
262
|
-
if (
|
|
502
|
+
// AST-based context check: skip templates inside safe callers
|
|
503
|
+
if (isInSafeCallerContext(node)) {
|
|
263
504
|
return;
|
|
264
505
|
}
|
|
265
|
-
//
|
|
266
|
-
if (!
|
|
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 (
|
|
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 =
|
|
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 (!
|
|
572
|
+
if (!containsGraphqlText(queryText)) {
|
|
328
573
|
return;
|
|
329
574
|
}
|
|
330
575
|
// Check for introspection queries
|
|
331
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
16
|
-
|
|
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', '
|
|
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', '
|
|
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', '
|
|
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
|
-
|
|
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:
|
|
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
|
|
156
|
-
if (
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
229
|
-
if (
|
|
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:
|
|
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
|
-
//
|
|
359
|
-
|
|
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
|