eslint-plugin-secure-coding 3.0.0 → 3.0.2
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/AGENTS.md +1 -1
- package/CHANGELOG.md +1 -1
- package/README.md +90 -422
- package/package.json +6 -5
- package/src/index.d.ts +5 -14
- package/src/index.js +36 -265
- package/src/rules/detect-non-literal-regexp/index.d.ts +20 -1
- package/src/rules/detect-non-literal-regexp/index.js +5 -0
- package/src/rules/detect-object-injection/index.d.ts +25 -1
- package/src/rules/detect-object-injection/index.js +5 -0
- package/src/rules/detect-weak-password-validation/index.d.ts +8 -2
- package/src/rules/detect-weak-password-validation/index.js +6 -1
- package/src/rules/no-directive-injection/index.d.ts +27 -1
- package/src/rules/no-directive-injection/index.js +5 -0
- package/src/rules/no-electron-security-issues/index.d.ts +27 -1
- package/src/rules/no-electron-security-issues/index.js +5 -0
- package/src/rules/no-format-string-injection/index.d.ts +28 -1
- package/src/rules/no-format-string-injection/index.js +5 -0
- package/src/rules/no-graphql-injection/index.d.ts +29 -1
- package/src/rules/no-graphql-injection/index.js +5 -0
- package/src/rules/no-hardcoded-credentials/index.d.ts +19 -1
- package/src/rules/no-hardcoded-credentials/index.js +5 -0
- package/src/rules/no-hardcoded-session-tokens/index.d.ts +8 -2
- package/src/rules/no-hardcoded-session-tokens/index.js +6 -1
- package/src/rules/no-improper-sanitization/index.d.ts +27 -1
- package/src/rules/no-improper-sanitization/index.js +5 -0
- package/src/rules/no-improper-type-validation/index.d.ts +27 -1
- package/src/rules/no-improper-type-validation/index.js +5 -0
- package/src/rules/no-insecure-comparison/index.d.ts +20 -1
- package/src/rules/no-insecure-comparison/index.js +5 -0
- package/src/rules/no-ldap-injection/index.d.ts +30 -1
- package/src/rules/no-ldap-injection/index.js +5 -0
- package/src/rules/no-missing-authentication/index.d.ts +20 -1
- package/src/rules/no-missing-authentication/index.js +5 -1
- package/src/rules/no-pii-in-logs/index.d.ts +8 -4
- package/src/rules/no-pii-in-logs/index.js +15 -12
- package/src/rules/no-privilege-escalation/index.d.ts +20 -1
- package/src/rules/no-privilege-escalation/index.js +5 -0
- package/src/rules/no-redos-vulnerable-regex/index.d.ts +22 -1
- package/src/rules/no-redos-vulnerable-regex/index.js +5 -0
- package/src/rules/no-sensitive-data-exposure/index.d.ts +20 -1
- package/src/rules/no-sensitive-data-exposure/index.js +5 -0
- package/src/rules/no-unchecked-loop-condition/index.d.ts +27 -1
- package/src/rules/no-unchecked-loop-condition/index.js +5 -0
- package/src/rules/no-unlimited-resource-allocation/index.d.ts +27 -1
- package/src/rules/no-unlimited-resource-allocation/index.js +5 -0
- package/src/rules/no-unsafe-deserialization/index.d.ts +31 -1
- package/src/rules/no-unsafe-deserialization/index.js +5 -0
- package/src/rules/no-unsafe-regex-construction/index.d.ts +22 -1
- package/src/rules/no-unsafe-regex-construction/index.js +5 -0
- package/src/rules/no-weak-password-recovery/index.d.ts +27 -1
- package/src/rules/no-weak-password-recovery/index.js +5 -0
- package/src/rules/no-xpath-injection/index.d.ts +30 -1
- package/src/rules/no-xpath-injection/index.js +5 -0
- package/src/rules/no-xxe-injection/index.d.ts +30 -1
- package/src/rules/no-xxe-injection/index.js +5 -0
- package/src/rules/require-backend-authorization/index.d.ts +8 -2
- package/src/rules/require-backend-authorization/index.js +6 -1
- package/src/rules/require-secure-defaults/index.d.ts +8 -4
- package/src/rules/require-secure-defaults/index.js +7 -6
- package/src/types/index.d.ts +10 -52
- package/src/types/index.js +3 -12
- package/src/rules/detect-child-process/index.d.ts +0 -11
- package/src/rules/detect-child-process/index.js +0 -529
- package/src/rules/detect-eval-with-expression/index.d.ts +0 -9
- package/src/rules/detect-eval-with-expression/index.js +0 -392
- package/src/rules/detect-mixed-content/index.d.ts +0 -8
- package/src/rules/detect-mixed-content/index.js +0 -44
- package/src/rules/detect-non-literal-fs-filename/index.d.ts +0 -7
- package/src/rules/detect-non-literal-fs-filename/index.js +0 -454
- package/src/rules/detect-suspicious-dependencies/index.d.ts +0 -8
- package/src/rules/detect-suspicious-dependencies/index.js +0 -71
- package/src/rules/no-allow-arbitrary-loads/index.d.ts +0 -8
- package/src/rules/no-allow-arbitrary-loads/index.js +0 -47
- package/src/rules/no-arbitrary-file-access/index.d.ts +0 -13
- package/src/rules/no-arbitrary-file-access/index.js +0 -195
- package/src/rules/no-buffer-overread/index.d.ts +0 -29
- package/src/rules/no-buffer-overread/index.js +0 -606
- package/src/rules/no-clickjacking/index.d.ts +0 -10
- package/src/rules/no-clickjacking/index.js +0 -396
- package/src/rules/no-client-side-auth-logic/index.d.ts +0 -6
- package/src/rules/no-client-side-auth-logic/index.js +0 -69
- package/src/rules/no-credentials-in-query-params/index.d.ts +0 -8
- package/src/rules/no-credentials-in-query-params/index.js +0 -57
- package/src/rules/no-data-in-temp-storage/index.d.ts +0 -6
- package/src/rules/no-data-in-temp-storage/index.js +0 -64
- package/src/rules/no-debug-code-in-production/index.d.ts +0 -8
- package/src/rules/no-debug-code-in-production/index.js +0 -51
- package/src/rules/no-disabled-certificate-validation/index.d.ts +0 -6
- package/src/rules/no-disabled-certificate-validation/index.js +0 -61
- package/src/rules/no-dynamic-dependency-loading/index.d.ts +0 -8
- package/src/rules/no-dynamic-dependency-loading/index.js +0 -51
- package/src/rules/no-exposed-debug-endpoints/index.d.ts +0 -6
- package/src/rules/no-exposed-debug-endpoints/index.js +0 -62
- package/src/rules/no-exposed-sensitive-data/index.d.ts +0 -11
- package/src/rules/no-exposed-sensitive-data/index.js +0 -340
- package/src/rules/no-http-urls/index.d.ts +0 -12
- package/src/rules/no-http-urls/index.js +0 -114
- package/src/rules/no-insecure-redirects/index.d.ts +0 -7
- package/src/rules/no-insecure-redirects/index.js +0 -216
- package/src/rules/no-insecure-websocket/index.d.ts +0 -6
- package/src/rules/no-insecure-websocket/index.js +0 -61
- package/src/rules/no-missing-cors-check/index.d.ts +0 -9
- package/src/rules/no-missing-cors-check/index.js +0 -399
- package/src/rules/no-missing-csrf-protection/index.d.ts +0 -11
- package/src/rules/no-missing-csrf-protection/index.js +0 -180
- package/src/rules/no-missing-security-headers/index.d.ts +0 -7
- package/src/rules/no-missing-security-headers/index.js +0 -218
- package/src/rules/no-password-in-url/index.d.ts +0 -8
- package/src/rules/no-password-in-url/index.js +0 -54
- package/src/rules/no-permissive-cors/index.d.ts +0 -8
- package/src/rules/no-permissive-cors/index.js +0 -65
- package/src/rules/no-sensitive-data-in-analytics/index.d.ts +0 -8
- package/src/rules/no-sensitive-data-in-analytics/index.js +0 -62
- package/src/rules/no-sensitive-data-in-cache/index.d.ts +0 -8
- package/src/rules/no-sensitive-data-in-cache/index.js +0 -52
- package/src/rules/no-toctou-vulnerability/index.d.ts +0 -7
- package/src/rules/no-toctou-vulnerability/index.js +0 -208
- package/src/rules/no-tracking-without-consent/index.d.ts +0 -6
- package/src/rules/no-tracking-without-consent/index.js +0 -67
- package/src/rules/no-unencrypted-transmission/index.d.ts +0 -11
- package/src/rules/no-unencrypted-transmission/index.js +0 -236
- package/src/rules/no-unescaped-url-parameter/index.d.ts +0 -9
- package/src/rules/no-unescaped-url-parameter/index.js +0 -355
- package/src/rules/no-unsafe-dynamic-require/index.d.ts +0 -5
- package/src/rules/no-unsafe-dynamic-require/index.js +0 -106
- package/src/rules/no-unvalidated-deeplinks/index.d.ts +0 -6
- package/src/rules/no-unvalidated-deeplinks/index.js +0 -62
- package/src/rules/no-unvalidated-user-input/index.d.ts +0 -9
- package/src/rules/no-unvalidated-user-input/index.js +0 -420
- package/src/rules/no-verbose-error-messages/index.d.ts +0 -8
- package/src/rules/no-verbose-error-messages/index.js +0 -68
- package/src/rules/no-zip-slip/index.d.ts +0 -9
- package/src/rules/no-zip-slip/index.js +0 -445
- package/src/rules/require-code-minification/index.d.ts +0 -8
- package/src/rules/require-code-minification/index.js +0 -47
- package/src/rules/require-csp-headers/index.d.ts +0 -6
- package/src/rules/require-csp-headers/index.js +0 -64
- package/src/rules/require-data-minimization/index.d.ts +0 -8
- package/src/rules/require-data-minimization/index.js +0 -53
- package/src/rules/require-dependency-integrity/index.d.ts +0 -6
- package/src/rules/require-dependency-integrity/index.js +0 -64
- package/src/rules/require-https-only/index.d.ts +0 -8
- package/src/rules/require-https-only/index.js +0 -62
- package/src/rules/require-mime-type-validation/index.d.ts +0 -6
- package/src/rules/require-mime-type-validation/index.js +0 -66
- package/src/rules/require-network-timeout/index.d.ts +0 -8
- package/src/rules/require-network-timeout/index.js +0 -50
- package/src/rules/require-package-lock/index.d.ts +0 -8
- package/src/rules/require-package-lock/index.js +0 -63
- package/src/rules/require-secure-credential-storage/index.d.ts +0 -8
- package/src/rules/require-secure-credential-storage/index.js +0 -50
- package/src/rules/require-secure-deletion/index.d.ts +0 -8
- package/src/rules/require-secure-deletion/index.js +0 -44
- package/src/rules/require-storage-encryption/index.d.ts +0 -8
- package/src/rules/require-storage-encryption/index.js +0 -50
- package/src/rules/require-url-validation/index.d.ts +0 -6
- package/src/rules/require-url-validation/index.js +0 -72
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.noUnescapedUrlParameter = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
6
|
-
/**
|
|
7
|
-
* Check if a node is inside a URL encoding function call
|
|
8
|
-
*/
|
|
9
|
-
function isInsideEncodingCall(node, sourceCode, trustedLibraries) {
|
|
10
|
-
let current = node;
|
|
11
|
-
while (current) {
|
|
12
|
-
if (current.type === 'CallExpression') {
|
|
13
|
-
const callee = current.callee;
|
|
14
|
-
// Check for encodeURIComponent, encodeURI
|
|
15
|
-
if (callee.type === 'Identifier') {
|
|
16
|
-
const calleeName = callee.name;
|
|
17
|
-
if (['encodeURIComponent', 'encodeURI', 'escape'].includes(calleeName)) {
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
// Check if it's a trusted library call
|
|
22
|
-
if (callee.type === 'MemberExpression') {
|
|
23
|
-
const object = callee.object;
|
|
24
|
-
if (object.type === 'Identifier') {
|
|
25
|
-
const objectName = object.name.toLowerCase();
|
|
26
|
-
if (trustedLibraries.some(lib => objectName.includes(lib.toLowerCase()))) {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
// Traverse up the AST
|
|
33
|
-
if ('parent' in current && current.parent) {
|
|
34
|
-
current = current.parent;
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Check if a string matches any ignore pattern
|
|
44
|
-
*/
|
|
45
|
-
function matchesIgnorePattern(text, ignorePatterns) {
|
|
46
|
-
return ignorePatterns.some(pattern => {
|
|
47
|
-
try {
|
|
48
|
-
const regex = new RegExp(pattern, 'i');
|
|
49
|
-
return regex.test(text);
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Check if a node is a URL construction pattern
|
|
58
|
-
*/
|
|
59
|
-
function isUrlConstruction(node, sourceCode) {
|
|
60
|
-
let text = sourceCode.getText(node);
|
|
61
|
-
// For template literals, combine raw strings to improve pattern detection
|
|
62
|
-
if (node.type === 'TemplateLiteral') {
|
|
63
|
-
text = node.quasis.map(q => q.value.raw).join('');
|
|
64
|
-
}
|
|
65
|
-
// Check for URL construction patterns
|
|
66
|
-
const urlPatterns = [
|
|
67
|
-
/\bhttps?:\/\//, // HTTP/HTTPS URLs
|
|
68
|
-
/\bnew\s+URL\s*\(/,
|
|
69
|
-
/\burl\s*[=:]\s*/, // url = or url:
|
|
70
|
-
/\burl\s*\+/, // url +
|
|
71
|
-
/\bwindow\.location/,
|
|
72
|
-
/\blocation\.href/,
|
|
73
|
-
/\bwindow\.open\s*\(/,
|
|
74
|
-
/\?[^=]+=/, // Query parameters
|
|
75
|
-
];
|
|
76
|
-
return urlPatterns.some(pattern => pattern.test(text));
|
|
77
|
-
}
|
|
78
|
-
exports.noUnescapedUrlParameter = (0, eslint_devkit_2.createRule)({
|
|
79
|
-
name: 'no-unescaped-url-parameter',
|
|
80
|
-
meta: {
|
|
81
|
-
type: 'problem',
|
|
82
|
-
docs: {
|
|
83
|
-
description: 'Detects unescaped URL parameters',
|
|
84
|
-
},
|
|
85
|
-
hasSuggestions: true,
|
|
86
|
-
messages: {
|
|
87
|
-
unescapedUrlParameter: (0, eslint_devkit_1.formatLLMMessage)({
|
|
88
|
-
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
89
|
-
issueName: 'Unescaped URL Parameter',
|
|
90
|
-
cwe: 'CWE-79',
|
|
91
|
-
description: 'Unescaped URL parameter detected: {{parameter}}',
|
|
92
|
-
severity: 'HIGH',
|
|
93
|
-
fix: '{{safeAlternative}}',
|
|
94
|
-
documentationLink: 'https://cwe.mitre.org/data/definitions/79.html',
|
|
95
|
-
}),
|
|
96
|
-
useEncodeURIComponent: (0, eslint_devkit_1.formatLLMMessage)({
|
|
97
|
-
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
98
|
-
issueName: 'Use encodeURIComponent',
|
|
99
|
-
description: 'Use encodeURIComponent for URL params',
|
|
100
|
-
severity: 'LOW',
|
|
101
|
-
fix: '`https://example.com?q=${encodeURIComponent(param)}`',
|
|
102
|
-
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent',
|
|
103
|
-
}),
|
|
104
|
-
useURLSearchParams: (0, eslint_devkit_1.formatLLMMessage)({
|
|
105
|
-
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
106
|
-
issueName: 'Use URLSearchParams',
|
|
107
|
-
description: 'Use URLSearchParams for safe URL construction',
|
|
108
|
-
severity: 'LOW',
|
|
109
|
-
fix: 'new URLSearchParams({ q: param }).toString()',
|
|
110
|
-
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams',
|
|
111
|
-
}),
|
|
112
|
-
},
|
|
113
|
-
schema: [
|
|
114
|
-
{
|
|
115
|
-
type: 'object',
|
|
116
|
-
properties: {
|
|
117
|
-
allowInTests: {
|
|
118
|
-
type: 'boolean',
|
|
119
|
-
default: false,
|
|
120
|
-
description: 'Allow unescaped URL parameters in test files',
|
|
121
|
-
},
|
|
122
|
-
trustedLibraries: {
|
|
123
|
-
type: 'array',
|
|
124
|
-
items: { type: 'string' },
|
|
125
|
-
default: ['url', 'querystring'],
|
|
126
|
-
description: 'Trusted URL construction libraries',
|
|
127
|
-
},
|
|
128
|
-
ignorePatterns: {
|
|
129
|
-
type: 'array',
|
|
130
|
-
items: { type: 'string' },
|
|
131
|
-
default: [],
|
|
132
|
-
description: 'Additional safe patterns to ignore',
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
additionalProperties: false,
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
},
|
|
139
|
-
defaultOptions: [
|
|
140
|
-
{
|
|
141
|
-
allowInTests: false,
|
|
142
|
-
trustedLibraries: ['url', 'querystring'],
|
|
143
|
-
ignorePatterns: [],
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
create(context, [options = {}]) {
|
|
147
|
-
const { allowInTests = false, trustedLibraries = ['url', 'querystring'], ignorePatterns = [], } = options;
|
|
148
|
-
const filename = context.getFilename();
|
|
149
|
-
const isTestFile = allowInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
|
|
150
|
-
const sourceCode = context.sourceCode || context.sourceCode;
|
|
151
|
-
function checkTemplateLiteral(node) {
|
|
152
|
-
if (isTestFile) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
// Check if this is a URL construction
|
|
156
|
-
if (!isUrlConstruction(node, sourceCode)) {
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
// Check each expression in the template
|
|
160
|
-
for (const expression of node.expressions) {
|
|
161
|
-
const text = sourceCode.getText(expression);
|
|
162
|
-
// Check if it matches any ignore pattern
|
|
163
|
-
if (matchesIgnorePattern(text, ignorePatterns)) {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
// Check if it's already encoded
|
|
167
|
-
if (isInsideEncodingCall(expression, sourceCode, trustedLibraries)) {
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
// Check if it's a user input pattern
|
|
171
|
-
const userInputPatterns = [
|
|
172
|
-
/\breq\.(query|params|body|headers|cookies)/,
|
|
173
|
-
/\brequest\.(query|params|body)/,
|
|
174
|
-
/\buserInput\b/i,
|
|
175
|
-
/\binput\b/i,
|
|
176
|
-
/\bsearchParams\b/,
|
|
177
|
-
/\bparam\b/i, // Generic param variable
|
|
178
|
-
];
|
|
179
|
-
// Check both the expression text and the full template literal
|
|
180
|
-
// This catches nested patterns like req.query.id
|
|
181
|
-
const fullText = sourceCode.getText(node);
|
|
182
|
-
const exprText = sourceCode.getText(expression);
|
|
183
|
-
const isUserInput = userInputPatterns.some(pattern => pattern.test(text)) ||
|
|
184
|
-
userInputPatterns.some(pattern => pattern.test(fullText)) ||
|
|
185
|
-
userInputPatterns.some(pattern => pattern.test(exprText)) ||
|
|
186
|
-
// Also check for nested member expressions like req.query.id
|
|
187
|
-
(expression.type === 'MemberExpression' &&
|
|
188
|
-
userInputPatterns.some(pattern => {
|
|
189
|
-
// Check the full expression including nested properties
|
|
190
|
-
return pattern.test(exprText);
|
|
191
|
-
}));
|
|
192
|
-
if (isUserInput) {
|
|
193
|
-
context.report({
|
|
194
|
-
node: expression,
|
|
195
|
-
messageId: 'unescapedUrlParameter',
|
|
196
|
-
data: {
|
|
197
|
-
parameter: text,
|
|
198
|
-
safeAlternative: `Use encodeURIComponent() or URLSearchParams: const url = \`https://example.com?q=\${encodeURIComponent(${text})}\`;`,
|
|
199
|
-
},
|
|
200
|
-
suggest: [
|
|
201
|
-
{
|
|
202
|
-
messageId: 'useEncodeURIComponent',
|
|
203
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
204
|
-
fix: (_fixer) => null,
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
messageId: 'useURLSearchParams',
|
|
208
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
209
|
-
fix: (_fixer) => null,
|
|
210
|
-
},
|
|
211
|
-
],
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
function checkBinaryExpression(node) {
|
|
217
|
-
if (isTestFile) {
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
// Check for string concatenation in URL construction
|
|
221
|
-
if (node.operator === '+') {
|
|
222
|
-
if (!isUrlConstruction(node, sourceCode)) {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
// Check right side (usually the parameter)
|
|
226
|
-
if (node.right.type !== 'Literal') {
|
|
227
|
-
const rightText = sourceCode.getText(node.right);
|
|
228
|
-
// Check if it matches any ignore pattern
|
|
229
|
-
if (matchesIgnorePattern(rightText, ignorePatterns)) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
// Check if it's already encoded
|
|
233
|
-
if (isInsideEncodingCall(node.right, sourceCode, trustedLibraries)) {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
// Check if it's a user input pattern
|
|
237
|
-
const userInputPatterns = [
|
|
238
|
-
/\breq\.(query|params|body)/,
|
|
239
|
-
/\brequest\.(query|params|body)/,
|
|
240
|
-
/\buserInput\b/,
|
|
241
|
-
/\binput\b/,
|
|
242
|
-
];
|
|
243
|
-
const isUserInput = userInputPatterns.some(pattern => pattern.test(rightText));
|
|
244
|
-
if (isUserInput) {
|
|
245
|
-
context.report({
|
|
246
|
-
node: node.right,
|
|
247
|
-
messageId: 'unescapedUrlParameter',
|
|
248
|
-
data: {
|
|
249
|
-
parameter: rightText,
|
|
250
|
-
safeAlternative: `Use encodeURIComponent(): ${sourceCode.getText(node.left)} + encodeURIComponent(${rightText})`,
|
|
251
|
-
},
|
|
252
|
-
suggest: [
|
|
253
|
-
{
|
|
254
|
-
messageId: 'useEncodeURIComponent',
|
|
255
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
256
|
-
fix: (_fixer) => null,
|
|
257
|
-
},
|
|
258
|
-
],
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
function isUserControlled(node, visited = new Set()) {
|
|
265
|
-
const text = sourceCode.getText(node);
|
|
266
|
-
const patterns = [
|
|
267
|
-
/\breq\.(query|params|body|headers|cookies)/,
|
|
268
|
-
/\brequest\.(query|params|body)/,
|
|
269
|
-
/\buserInput\b/i,
|
|
270
|
-
/\binput\b/i,
|
|
271
|
-
/\bsearchParams\b/,
|
|
272
|
-
/\bparam\b/i,
|
|
273
|
-
/\breturnUrl\b/i,
|
|
274
|
-
/\burl\b/i,
|
|
275
|
-
/\bredirect\b/i,
|
|
276
|
-
/\bnext\b/i,
|
|
277
|
-
];
|
|
278
|
-
if (patterns.some(p => p.test(text)))
|
|
279
|
-
return true;
|
|
280
|
-
// Trace identifiers
|
|
281
|
-
if (node.type === 'Identifier') {
|
|
282
|
-
if (visited.has(node.name))
|
|
283
|
-
return false;
|
|
284
|
-
visited.add(node.name);
|
|
285
|
-
const scope = sourceCode.getScope(node);
|
|
286
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
287
|
-
const variable = scope.variables.find((v) => v.name === node.name);
|
|
288
|
-
if (variable && variable.defs.length > 0) {
|
|
289
|
-
const def = variable.defs[0];
|
|
290
|
-
if (def.type === 'Variable' && def.node.init) {
|
|
291
|
-
const init = def.node.init;
|
|
292
|
-
// Check if init is constructed from other user inputs
|
|
293
|
-
if (isUserControlled(init, visited))
|
|
294
|
-
return true;
|
|
295
|
-
// Check if init is a TemplateLiteral containing user inputs in expressions
|
|
296
|
-
if (init.type === 'TemplateLiteral') {
|
|
297
|
-
return init.expressions.some(expr => isUserControlled(expr, visited));
|
|
298
|
-
}
|
|
299
|
-
// Check if init is BinaryExpression (concatenation)
|
|
300
|
-
if (init.type === 'BinaryExpression') {
|
|
301
|
-
return isUserControlled(init.left, visited) || isUserControlled(init.right, visited);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
return {
|
|
309
|
-
TemplateLiteral: checkTemplateLiteral,
|
|
310
|
-
BinaryExpression: checkBinaryExpression,
|
|
311
|
-
AssignmentExpression(node) {
|
|
312
|
-
if (isTestFile)
|
|
313
|
-
return;
|
|
314
|
-
// Check for window.location = ... or window.location.href = ...
|
|
315
|
-
const left = node.left;
|
|
316
|
-
let isLocationAssignment = false;
|
|
317
|
-
if (left.type === 'MemberExpression') {
|
|
318
|
-
const objectName = left.object.type === 'Identifier' ? left.object.name :
|
|
319
|
-
(left.object.type === 'MemberExpression' ? sourceCode.getText(left.object) : '');
|
|
320
|
-
const propName = left.property.type === 'Identifier' ? left.property.name : '';
|
|
321
|
-
if ((objectName === 'window' && propName === 'location') ||
|
|
322
|
-
(propName === 'href' && objectName.includes('location'))) {
|
|
323
|
-
isLocationAssignment = true;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
else if (left.type === 'Identifier' && left.name === 'location') {
|
|
327
|
-
// In browser location = ... is valid
|
|
328
|
-
isLocationAssignment = true;
|
|
329
|
-
}
|
|
330
|
-
if (isLocationAssignment) {
|
|
331
|
-
const right = node.right;
|
|
332
|
-
const rightText = sourceCode.getText(right);
|
|
333
|
-
// Skip TemplateLiteral and BinaryExpression as they are covered by their own visitors
|
|
334
|
-
if (right.type === 'TemplateLiteral' || right.type === 'BinaryExpression') {
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
if (matchesIgnorePattern(rightText, ignorePatterns))
|
|
338
|
-
return;
|
|
339
|
-
if (isInsideEncodingCall(right, sourceCode, trustedLibraries))
|
|
340
|
-
return;
|
|
341
|
-
if (isUserControlled(right)) {
|
|
342
|
-
context.report({
|
|
343
|
-
node: right,
|
|
344
|
-
messageId: 'unescapedUrlParameter',
|
|
345
|
-
data: {
|
|
346
|
-
parameter: rightText,
|
|
347
|
-
safeAlternative: 'Validate and encode URL before redirecting',
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
},
|
|
355
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.noUnsafeDynamicRequire = void 0;
|
|
4
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
5
|
-
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
6
|
-
exports.noUnsafeDynamicRequire = (0, eslint_devkit_2.createRule)({
|
|
7
|
-
name: 'no-unsafe-dynamic-require',
|
|
8
|
-
meta: {
|
|
9
|
-
type: 'problem',
|
|
10
|
-
docs: {
|
|
11
|
-
description: 'Prevent unsafe dynamic require() calls that could enable code injection',
|
|
12
|
-
},
|
|
13
|
-
fixable: 'code',
|
|
14
|
-
hasSuggestions: false,
|
|
15
|
-
messages: {
|
|
16
|
-
unsafeDynamicRequire: (0, eslint_devkit_1.formatLLMMessage)({
|
|
17
|
-
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
18
|
-
issueName: 'Dynamic require()',
|
|
19
|
-
cwe: 'CWE-95',
|
|
20
|
-
description: 'Dynamic require() detected',
|
|
21
|
-
severity: 'CRITICAL',
|
|
22
|
-
fix: 'Use allowlist: const ALLOWED = ["mod1", "mod2"]; if (!ALLOWED.includes(name)) throw Error("Not allowed")',
|
|
23
|
-
documentationLink: 'https://owasp.org/www-community/attacks/Code_Injection',
|
|
24
|
-
}),
|
|
25
|
-
},
|
|
26
|
-
schema: [
|
|
27
|
-
{
|
|
28
|
-
type: 'object',
|
|
29
|
-
properties: {
|
|
30
|
-
allowDynamicImport: {
|
|
31
|
-
type: 'boolean',
|
|
32
|
-
default: false,
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
additionalProperties: false,
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
},
|
|
39
|
-
defaultOptions: [
|
|
40
|
-
{
|
|
41
|
-
allowDynamicImport: false,
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
create(context) {
|
|
45
|
-
/**
|
|
46
|
-
* Track variables that reference require
|
|
47
|
-
* Maps variable name to the node where it was assigned
|
|
48
|
-
*/
|
|
49
|
-
const requireVariables = new Set();
|
|
50
|
-
/**
|
|
51
|
-
* Check if a node is a reference to require
|
|
52
|
-
*/
|
|
53
|
-
const isRequireReference = (node) => {
|
|
54
|
-
if (node.type === 'Identifier' && node.name === 'require') {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
if (node.type === 'Identifier' && requireVariables.has(node.name)) {
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
return false;
|
|
61
|
-
};
|
|
62
|
-
/**
|
|
63
|
-
* Check if argument is dynamic (not a literal)
|
|
64
|
-
*/
|
|
65
|
-
const isDynamicArgument = (arg) => {
|
|
66
|
-
if (arg.type === 'Literal')
|
|
67
|
-
return false;
|
|
68
|
-
if (arg.type === 'TemplateLiteral' && arg.expressions.length === 0)
|
|
69
|
-
return false;
|
|
70
|
-
return true;
|
|
71
|
-
};
|
|
72
|
-
return {
|
|
73
|
-
VariableDeclarator(node) {
|
|
74
|
-
// Track when require is assigned to a variable
|
|
75
|
-
if (node.id.type === 'Identifier' && node.init) {
|
|
76
|
-
if (node.init.type === 'Identifier' && node.init.name === 'require') {
|
|
77
|
-
requireVariables.add(node.id.name);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
CallExpression(node) {
|
|
82
|
-
// Check for require() calls (direct or via variable)
|
|
83
|
-
if (node.callee.type !== 'Identifier') {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
// Check if callee is require or a variable that references require
|
|
87
|
-
if (!isRequireReference(node.callee)) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
// Must have at least one argument
|
|
91
|
-
if (node.arguments.length === 0)
|
|
92
|
-
return;
|
|
93
|
-
const firstArg = node.arguments[0];
|
|
94
|
-
if (firstArg.type === 'SpreadElement')
|
|
95
|
-
return;
|
|
96
|
-
// Check if dynamic
|
|
97
|
-
if (!isDynamicArgument(firstArg))
|
|
98
|
-
return;
|
|
99
|
-
context.report({
|
|
100
|
-
node,
|
|
101
|
-
messageId: 'unsafeDynamicRequire',
|
|
102
|
-
});
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
},
|
|
106
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview Require validation of deep link URLs
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.noUnvalidatedDeeplinks = void 0;
|
|
7
|
-
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
8
|
-
exports.noUnvalidatedDeeplinks = (0, eslint_devkit_1.createRule)({
|
|
9
|
-
name: 'no-unvalidated-deeplinks',
|
|
10
|
-
meta: {
|
|
11
|
-
type: 'problem',
|
|
12
|
-
docs: {
|
|
13
|
-
description: 'Require validation of deep link URLs',
|
|
14
|
-
},
|
|
15
|
-
messages: {
|
|
16
|
-
violationDetected: (0, eslint_devkit_1.formatLLMMessage)({
|
|
17
|
-
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
18
|
-
issueName: 'Unvalidated Deeplink',
|
|
19
|
-
cwe: 'CWE-939',
|
|
20
|
-
description: 'Deep link URL used without validation - potential open redirect',
|
|
21
|
-
severity: 'HIGH',
|
|
22
|
-
fix: 'Validate deep link URLs against a whitelist before navigation',
|
|
23
|
-
documentationLink: 'https://cwe.mitre.org/data/definitions/939.html',
|
|
24
|
-
})
|
|
25
|
-
},
|
|
26
|
-
schema: [],
|
|
27
|
-
},
|
|
28
|
-
defaultOptions: [],
|
|
29
|
-
create(context) {
|
|
30
|
-
function report(node) {
|
|
31
|
-
context.report({ node, messageId: 'violationDetected' });
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
CallExpression(node) {
|
|
35
|
-
// Detect Linking.openURL() with variable argument (React Native)
|
|
36
|
-
if (node.callee.type === 'MemberExpression' &&
|
|
37
|
-
node.callee.object.type === 'Identifier' &&
|
|
38
|
-
node.callee.object.name === 'Linking' &&
|
|
39
|
-
node.callee.property.type === 'Identifier' &&
|
|
40
|
-
node.callee.property.name === 'openURL') {
|
|
41
|
-
const urlArg = node.arguments[0];
|
|
42
|
-
// Flag if URL is a variable/expression, not a literal
|
|
43
|
-
if (urlArg && urlArg.type === 'Identifier') {
|
|
44
|
-
report(node);
|
|
45
|
-
}
|
|
46
|
-
if (urlArg && urlArg.type === 'MemberExpression') {
|
|
47
|
-
report(node);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
// Detect navigation.navigate with external URLs
|
|
51
|
-
if (node.callee.type === 'MemberExpression' &&
|
|
52
|
-
node.callee.property.type === 'Identifier' &&
|
|
53
|
-
node.callee.property.name === 'navigate') {
|
|
54
|
-
const urlArg = node.arguments[0];
|
|
55
|
-
if (urlArg && urlArg.type === 'Identifier') {
|
|
56
|
-
report(node);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
},
|
|
62
|
-
});
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export interface Options {
|
|
2
|
-
/** Allow unvalidated input in test files. Default: false */
|
|
3
|
-
allowInTests?: boolean;
|
|
4
|
-
/** Trusted validation libraries. Default: ['zod', 'joi', 'yup', 'class-validator'] */
|
|
5
|
-
trustedLibraries?: string[];
|
|
6
|
-
/** Additional safe patterns to ignore. Default: ['^safe', '^sanitized', '^validated', '^clean'] (prefix patterns) */
|
|
7
|
-
ignorePatterns?: string[];
|
|
8
|
-
}
|
|
9
|
-
export declare const noUnvalidatedUserInput: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener>;
|