@sun-asterisk/sunlint 1.3.1 → 1.3.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/CHANGELOG.md +47 -0
- package/CONTRIBUTING.md +210 -1691
- package/config/rule-analysis-strategies.js +17 -1
- package/config/rules/enhanced-rules-registry.json +369 -1135
- package/config/rules/rules-registry-generated.json +1 -1
- package/core/enhanced-rules-registry.js +2 -1
- package/core/semantic-engine.js +15 -3
- package/core/semantic-rule-base.js +4 -2
- package/engines/heuristic-engine.js +65 -4
- package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
- package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
- package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
- package/origin-rules/common-en.md +11 -7
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
- package/rules/common/C006_function_naming/analyzer.js +29 -3
- package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
- package/rules/common/C010_limit_block_nesting/config.json +64 -0
- package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
- package/rules/common/C013_no_dead_code/analyzer.js +75 -177
- package/rules/common/C013_no_dead_code/config.json +61 -0
- package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
- package/rules/common/C014_dependency_injection/analyzer.js +48 -313
- package/rules/common/C014_dependency_injection/config.json +26 -0
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
- package/rules/common/C018_no_throw_generic_error/config.json +50 -0
- package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +110 -317
- package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
- package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
- package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
- package/rules/common/C023_no_duplicate_variable/config.json +50 -0
- package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
- package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
- package/rules/common/C035_error_logging_context/analyzer.js +3 -1
- package/rules/index.js +5 -1
- package/rules/security/S009_no_insecure_encryption/README.md +158 -0
- package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
- package/rules/security/S009_no_insecure_encryption/config.json +55 -0
- package/rules/security/S010_no_insecure_encryption/README.md +224 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
- package/rules/security/S010_no_insecure_encryption/config.json +48 -0
- package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
- package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
- package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
- package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
- package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
- package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
- package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
- package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
- package/rules/security/S055_content_type_validation/README.md +176 -0
- package/rules/security/S055_content_type_validation/analyzer.js +312 -0
- package/rules/security/S055_content_type_validation/config.json +48 -0
- package/rules/utils/rule-helpers.js +140 -1
- package/scripts/consolidate-config.js +116 -0
- package/config/rules/S027-categories.json +0 -122
- package/config/rules/rules-registry.json +0 -777
- package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S016 Symbol-based Analyzer - Sensitive Data in URL Query Parameters Detection
|
|
3
|
+
* Purpose: Use AST + Symbol Resolution to detect sensitive data passed via query strings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { SyntaxKind } = require("ts-morph");
|
|
7
|
+
|
|
8
|
+
class S016SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.ruleId = "S016";
|
|
11
|
+
this.ruleName = "Sensitive Data in URL Query Parameters (Symbol-Based)";
|
|
12
|
+
this.semanticEngine = semanticEngine;
|
|
13
|
+
this.verbose = false;
|
|
14
|
+
|
|
15
|
+
// URL construction patterns
|
|
16
|
+
this.urlPatterns = {
|
|
17
|
+
// Direct URL construction
|
|
18
|
+
urlConstructor: ["URL", "new URL"],
|
|
19
|
+
urlSearchParams: ["URLSearchParams", "new URLSearchParams"],
|
|
20
|
+
|
|
21
|
+
// HTTP client libraries
|
|
22
|
+
fetch: ["fetch"],
|
|
23
|
+
axios: [
|
|
24
|
+
"axios.get",
|
|
25
|
+
"axios.post",
|
|
26
|
+
"axios.put",
|
|
27
|
+
"axios.delete",
|
|
28
|
+
"axios.patch",
|
|
29
|
+
"axios.request",
|
|
30
|
+
],
|
|
31
|
+
request: ["request", "request.get", "request.post"],
|
|
32
|
+
|
|
33
|
+
// Node.js modules
|
|
34
|
+
http: ["http.get", "http.request", "https.get", "https.request"],
|
|
35
|
+
querystring: ["querystring.stringify", "qs.stringify"],
|
|
36
|
+
|
|
37
|
+
// Framework specific
|
|
38
|
+
express: ["res.redirect", "req.query"],
|
|
39
|
+
nextjs: ["router.push", "router.replace", "Link"],
|
|
40
|
+
react: ["window.location.href", "location.href"],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Sensitive data patterns (more comprehensive)
|
|
44
|
+
this.sensitivePatterns = [
|
|
45
|
+
// Authentication & Authorization
|
|
46
|
+
"password",
|
|
47
|
+
"passwd",
|
|
48
|
+
"pwd",
|
|
49
|
+
"pass",
|
|
50
|
+
"token",
|
|
51
|
+
"jwt",
|
|
52
|
+
"accesstoken",
|
|
53
|
+
"refreshtoken",
|
|
54
|
+
"bearertoken",
|
|
55
|
+
"secret",
|
|
56
|
+
"secretkey",
|
|
57
|
+
"clientsecret",
|
|
58
|
+
"serversecret",
|
|
59
|
+
"apikey",
|
|
60
|
+
"api_key",
|
|
61
|
+
"key",
|
|
62
|
+
"privatekey",
|
|
63
|
+
"publickey",
|
|
64
|
+
"auth",
|
|
65
|
+
"authorization",
|
|
66
|
+
"authenticate",
|
|
67
|
+
"sessionid",
|
|
68
|
+
"session_id",
|
|
69
|
+
"jsessionid",
|
|
70
|
+
"csrf",
|
|
71
|
+
"csrftoken",
|
|
72
|
+
"xsrf",
|
|
73
|
+
|
|
74
|
+
// Financial & Personal
|
|
75
|
+
"ssn",
|
|
76
|
+
"social",
|
|
77
|
+
"socialsecurity",
|
|
78
|
+
"creditcard",
|
|
79
|
+
"cardnumber",
|
|
80
|
+
"cardnum",
|
|
81
|
+
"ccnumber",
|
|
82
|
+
"cvv",
|
|
83
|
+
"cvc",
|
|
84
|
+
"cvd",
|
|
85
|
+
"cid",
|
|
86
|
+
"pin",
|
|
87
|
+
"pincode",
|
|
88
|
+
"bankaccount",
|
|
89
|
+
"routing",
|
|
90
|
+
"iban",
|
|
91
|
+
|
|
92
|
+
// Personal Identifiable Information
|
|
93
|
+
"email",
|
|
94
|
+
"emailaddress",
|
|
95
|
+
"mail",
|
|
96
|
+
"phone",
|
|
97
|
+
"phonenumber",
|
|
98
|
+
"mobile",
|
|
99
|
+
"tel",
|
|
100
|
+
"address",
|
|
101
|
+
"homeaddress",
|
|
102
|
+
"zipcode",
|
|
103
|
+
"postal",
|
|
104
|
+
"birthdate",
|
|
105
|
+
"birthday",
|
|
106
|
+
"dob",
|
|
107
|
+
"license",
|
|
108
|
+
"passport",
|
|
109
|
+
"identity",
|
|
110
|
+
|
|
111
|
+
// Business sensitive
|
|
112
|
+
"salary",
|
|
113
|
+
"income",
|
|
114
|
+
"wage",
|
|
115
|
+
"medical",
|
|
116
|
+
"health",
|
|
117
|
+
"diagnosis",
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
// Query parameter indicators
|
|
121
|
+
this.queryIndicators = [
|
|
122
|
+
"query",
|
|
123
|
+
"params",
|
|
124
|
+
"search",
|
|
125
|
+
"searchparams",
|
|
126
|
+
"urlparams",
|
|
127
|
+
"querystring",
|
|
128
|
+
"qs",
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async initialize(semanticEngine = null) {
|
|
133
|
+
if (semanticEngine) {
|
|
134
|
+
this.semanticEngine = semanticEngine;
|
|
135
|
+
}
|
|
136
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
137
|
+
|
|
138
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
139
|
+
console.log(
|
|
140
|
+
`🔧 [S016 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
146
|
+
return await this.analyzeFileWithSymbols(filePath, options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async analyzeFileWithSymbols(filePath, options = {}) {
|
|
150
|
+
const violations = [];
|
|
151
|
+
|
|
152
|
+
const verbose = options.verbose || this.verbose;
|
|
153
|
+
|
|
154
|
+
if (!this.semanticEngine?.project) {
|
|
155
|
+
if (verbose) {
|
|
156
|
+
console.warn(
|
|
157
|
+
"[S016 Symbol-Based] No semantic engine available, skipping analysis"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return violations;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (verbose) {
|
|
164
|
+
console.log(`🔍 [S016 Symbol-Based] Starting analysis for ${filePath}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
169
|
+
if (!sourceFile) {
|
|
170
|
+
return violations;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Find various URL/query construction patterns
|
|
174
|
+
const urlConstructions = this.findUrlConstructions(sourceFile, verbose);
|
|
175
|
+
const queryStringUsages = this.findQueryStringUsages(sourceFile, verbose);
|
|
176
|
+
const httpClientCalls = this.findHttpClientCalls(sourceFile, verbose);
|
|
177
|
+
|
|
178
|
+
if (verbose) {
|
|
179
|
+
console.log(
|
|
180
|
+
`🔍 [S016 Symbol-Based] Found ${urlConstructions.length} URL constructions, ${queryStringUsages.length} query usages, ${httpClientCalls.length} HTTP calls`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Analyze each pattern
|
|
185
|
+
const allPatterns = [
|
|
186
|
+
...urlConstructions,
|
|
187
|
+
...queryStringUsages,
|
|
188
|
+
...httpClientCalls,
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
for (const pattern of allPatterns) {
|
|
192
|
+
const patternViolations = this.analyzeUrlPattern(
|
|
193
|
+
pattern,
|
|
194
|
+
sourceFile,
|
|
195
|
+
filePath,
|
|
196
|
+
verbose
|
|
197
|
+
);
|
|
198
|
+
violations.push(...patternViolations);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (verbose) {
|
|
202
|
+
console.log(
|
|
203
|
+
`🔍 [S016 Symbol-Based] Total violations found: ${violations.length}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return violations;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (verbose) {
|
|
210
|
+
console.warn(
|
|
211
|
+
`[S016 Symbol-Based] Analysis failed for ${filePath}:`,
|
|
212
|
+
error.message
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return violations;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Find URL construction patterns (new URL, URLSearchParams, etc.)
|
|
221
|
+
*/
|
|
222
|
+
findUrlConstructions(sourceFile, verbose = false) {
|
|
223
|
+
const patterns = [];
|
|
224
|
+
|
|
225
|
+
// Find 'new URL()' constructions
|
|
226
|
+
const newExpressions = sourceFile.getDescendantsOfKind(
|
|
227
|
+
SyntaxKind.NewExpression
|
|
228
|
+
);
|
|
229
|
+
for (const newExpr of newExpressions) {
|
|
230
|
+
const identifier = newExpr.getExpression();
|
|
231
|
+
if (
|
|
232
|
+
identifier.getText() === "URL" ||
|
|
233
|
+
identifier.getText() === "URLSearchParams"
|
|
234
|
+
) {
|
|
235
|
+
patterns.push({
|
|
236
|
+
type: "constructor",
|
|
237
|
+
node: newExpr,
|
|
238
|
+
method: identifier.getText(),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (verbose) {
|
|
244
|
+
console.log(
|
|
245
|
+
`🔍 [S016 Symbol-Based] Found ${patterns.length} URL constructor patterns`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return patterns;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Find query string manipulation patterns
|
|
254
|
+
*/
|
|
255
|
+
findQueryStringUsages(sourceFile, verbose = false) {
|
|
256
|
+
const patterns = [];
|
|
257
|
+
|
|
258
|
+
// Find property access expressions that might involve query strings
|
|
259
|
+
const propertyAccess = sourceFile.getDescendantsOfKind(
|
|
260
|
+
SyntaxKind.PropertyAccessExpression
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
for (const propAccess of propertyAccess) {
|
|
264
|
+
const fullText = propAccess.getText().toLowerCase();
|
|
265
|
+
|
|
266
|
+
// Check for query-related property access
|
|
267
|
+
if (
|
|
268
|
+
this.queryIndicators.some((indicator) => fullText.includes(indicator))
|
|
269
|
+
) {
|
|
270
|
+
patterns.push({
|
|
271
|
+
type: "property_access",
|
|
272
|
+
node: propAccess,
|
|
273
|
+
method: propAccess.getText(),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Check for location.search, window.location.search, etc.
|
|
278
|
+
if (fullText.includes("search") || fullText.includes("query")) {
|
|
279
|
+
patterns.push({
|
|
280
|
+
type: "location_search",
|
|
281
|
+
node: propAccess,
|
|
282
|
+
method: propAccess.getText(),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (verbose) {
|
|
288
|
+
console.log(
|
|
289
|
+
`🔍 [S016 Symbol-Based] Found ${patterns.length} query string usage patterns`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return patterns;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Find HTTP client calls that might include query parameters
|
|
298
|
+
*/
|
|
299
|
+
findHttpClientCalls(sourceFile, verbose = false) {
|
|
300
|
+
const patterns = [];
|
|
301
|
+
|
|
302
|
+
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
303
|
+
SyntaxKind.CallExpression
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
for (const callExpr of callExpressions) {
|
|
307
|
+
const expression = callExpr.getExpression();
|
|
308
|
+
const callText = expression.getText().toLowerCase();
|
|
309
|
+
|
|
310
|
+
// Check against known HTTP client patterns
|
|
311
|
+
for (const [client, methods] of Object.entries(this.urlPatterns)) {
|
|
312
|
+
for (const method of methods) {
|
|
313
|
+
if (callText.includes(method.toLowerCase())) {
|
|
314
|
+
patterns.push({
|
|
315
|
+
type: "http_client",
|
|
316
|
+
node: callExpr,
|
|
317
|
+
method: method,
|
|
318
|
+
client: client,
|
|
319
|
+
});
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (verbose) {
|
|
327
|
+
console.log(
|
|
328
|
+
`🔍 [S016 Symbol-Based] Found ${patterns.length} HTTP client call patterns`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return patterns;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Analyze URL pattern for sensitive data in query parameters
|
|
337
|
+
*/
|
|
338
|
+
analyzeUrlPattern(pattern, sourceFile, filePath, verbose = false) {
|
|
339
|
+
const violations = [];
|
|
340
|
+
const lineNumber = pattern.node.getStartLineNumber();
|
|
341
|
+
const columnNumber =
|
|
342
|
+
pattern.node.getStart() - pattern.node.getStartLinePos();
|
|
343
|
+
|
|
344
|
+
if (verbose) {
|
|
345
|
+
console.log(
|
|
346
|
+
`🔍 [S016 Symbol-Based] Analyzing ${pattern.type} pattern: ${pattern.method}`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Only check for sensitive keys in actual query string
|
|
351
|
+
let queryString = "";
|
|
352
|
+
let sensitiveParams = [];
|
|
353
|
+
|
|
354
|
+
if (pattern.type === "constructor" || pattern.type === "http_client") {
|
|
355
|
+
const args = pattern.node.getArguments?.() || [];
|
|
356
|
+
// Only check first argument (URL)
|
|
357
|
+
if (args.length > 0) {
|
|
358
|
+
const urlText = args[0].getText();
|
|
359
|
+
const match = urlText.match(/\?(.*)/);
|
|
360
|
+
if (match && match[1]) {
|
|
361
|
+
queryString = match[1];
|
|
362
|
+
// Split query string into keys
|
|
363
|
+
const keys = queryString
|
|
364
|
+
.split("&")
|
|
365
|
+
.map((pair) => pair.split("=")[0].toLowerCase());
|
|
366
|
+
sensitiveParams = keys.filter((key) =>
|
|
367
|
+
this.sensitivePatterns.includes(key)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} else if (
|
|
372
|
+
pattern.type === "property_access" ||
|
|
373
|
+
pattern.type === "location_search"
|
|
374
|
+
) {
|
|
375
|
+
// Only check if .searchParams.set or .query is used with sensitive key
|
|
376
|
+
const methodText = pattern.method.toLowerCase();
|
|
377
|
+
for (const sensitiveKey of this.sensitivePatterns) {
|
|
378
|
+
// Only match if set as key in searchParams or query
|
|
379
|
+
const regex = new RegExp(`\.set\(['"]${sensitiveKey}['"]`, "i");
|
|
380
|
+
if (regex.test(methodText)) {
|
|
381
|
+
sensitiveParams.push(sensitiveKey);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (sensitiveParams.length > 0) {
|
|
387
|
+
violations.push({
|
|
388
|
+
ruleId: this.ruleId,
|
|
389
|
+
severity: "error",
|
|
390
|
+
message: "Sensitive data detected in URL query parameters",
|
|
391
|
+
source: this.ruleId,
|
|
392
|
+
file: filePath,
|
|
393
|
+
line: lineNumber,
|
|
394
|
+
column: columnNumber,
|
|
395
|
+
description: `[SYMBOL-BASED] Sensitive parameters detected: ${sensitiveParams.join(
|
|
396
|
+
", "
|
|
397
|
+
)}. This can expose data in logs, browser history, and network traces.`,
|
|
398
|
+
suggestion:
|
|
399
|
+
"Move sensitive data to request body (POST/PUT) or use secure headers. For authentication, use proper header-based tokens.",
|
|
400
|
+
category: "security",
|
|
401
|
+
patternType: pattern.type,
|
|
402
|
+
method: pattern.method,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Additional checks for specific patterns
|
|
407
|
+
if (
|
|
408
|
+
pattern.type === "constructor" &&
|
|
409
|
+
pattern.method === "URLSearchParams"
|
|
410
|
+
) {
|
|
411
|
+
// Special handling for URLSearchParams constructor
|
|
412
|
+
const constructorViolations = this.analyzeURLSearchParamsConstructor(
|
|
413
|
+
pattern.node,
|
|
414
|
+
filePath,
|
|
415
|
+
verbose
|
|
416
|
+
);
|
|
417
|
+
violations.push(...constructorViolations);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return violations;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Analyze URLSearchParams constructor specifically
|
|
425
|
+
*/
|
|
426
|
+
analyzeURLSearchParamsConstructor(node, filePath, verbose = false) {
|
|
427
|
+
const violations = [];
|
|
428
|
+
const args = node.getArguments();
|
|
429
|
+
|
|
430
|
+
if (args.length === 0) return violations;
|
|
431
|
+
|
|
432
|
+
const firstArg = args[0];
|
|
433
|
+
|
|
434
|
+
// If first argument is an object literal, check its properties
|
|
435
|
+
if (firstArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
436
|
+
const properties = firstArg.getProperties();
|
|
437
|
+
|
|
438
|
+
for (const prop of properties) {
|
|
439
|
+
if (prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
440
|
+
const propName = prop.getName()?.toLowerCase() || "";
|
|
441
|
+
|
|
442
|
+
const matchingSensitivePattern = this.sensitivePatterns.find(
|
|
443
|
+
(pattern) => {
|
|
444
|
+
const regex = new RegExp(`\\b${pattern}\\b`, "i");
|
|
445
|
+
return regex.test(propName);
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
if (matchingSensitivePattern) {
|
|
450
|
+
violations.push({
|
|
451
|
+
ruleId: this.ruleId,
|
|
452
|
+
severity: "error",
|
|
453
|
+
message: `Sensitive parameter '${propName}' in URLSearchParams constructor`,
|
|
454
|
+
source: this.ruleId,
|
|
455
|
+
file: filePath,
|
|
456
|
+
line: prop.getStartLineNumber(),
|
|
457
|
+
column: prop.getStart() - prop.getStartLinePos(),
|
|
458
|
+
description: `[SYMBOL-BASED] Parameter '${propName}' contains sensitive data pattern '${matchingSensitivePattern}'. URLSearchParams will be visible in URLs.`,
|
|
459
|
+
suggestion:
|
|
460
|
+
"Move sensitive parameters to request body or secure headers",
|
|
461
|
+
category: "security",
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return violations;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Find sensitive parameters in content
|
|
473
|
+
*/
|
|
474
|
+
findSensitiveParameters(content, verbose = false) {
|
|
475
|
+
const sensitiveParams = [];
|
|
476
|
+
const lowerContent = content.toLowerCase();
|
|
477
|
+
|
|
478
|
+
for (const pattern of this.sensitivePatterns) {
|
|
479
|
+
// Use word boundaries to avoid false positives
|
|
480
|
+
const regex = new RegExp(`\\b${pattern}\\b`, "i");
|
|
481
|
+
if (regex.test(lowerContent)) {
|
|
482
|
+
sensitiveParams.push(pattern);
|
|
483
|
+
if (verbose) {
|
|
484
|
+
console.log(
|
|
485
|
+
`🔍 [S016 Symbol-Based] Sensitive pattern detected: '${pattern}'`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return [...new Set(sensitiveParams)]; // Remove duplicates
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
module.exports = S016SymbolBasedAnalyzer;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# S048 - No Current Password in Reset Process
|
|
2
|
+
|
|
3
|
+
## Mô tả
|
|
4
|
+
|
|
5
|
+
Rule này kiểm tra xem các quy trình đặt lại mật khẩu có yêu cầu mật khẩu hiện tại hay không. Việc yêu cầu mật khẩu hiện tại trong quy trình reset mật khẩu vi phạm nguyên tắc bảo mật và làm mất đi mục đích của tính năng "quên mật khẩu".
|
|
6
|
+
|
|
7
|
+
## Mục tiêu
|
|
8
|
+
|
|
9
|
+
- Ngăn chặn việc yêu cầu mật khẩu hiện tại trong quy trình reset mật khẩu
|
|
10
|
+
- Đảm bảo quy trình reset mật khẩu được thiết kế an toàn và hợp lý
|
|
11
|
+
- Tuân thủ OWASP A04:2021 - Insecure Design và CWE-640
|
|
12
|
+
|
|
13
|
+
## Chi tiết Rule
|
|
14
|
+
|
|
15
|
+
### Phát hiện lỗi khi:
|
|
16
|
+
|
|
17
|
+
1. **API endpoints yêu cầu current password trong reset**:
|
|
18
|
+
```javascript
|
|
19
|
+
app.post('/reset-password', (req, res) => {
|
|
20
|
+
const { currentPassword, newPassword } = req.body; // ❌ Yêu cầu mật khẩu hiện tại
|
|
21
|
+
if (!validateCurrentPassword(currentPassword)) {
|
|
22
|
+
return res.status(400).json({ error: 'Current password incorrect' });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. **Form validation yêu cầu current password**:
|
|
28
|
+
```typescript
|
|
29
|
+
const resetPasswordSchema = {
|
|
30
|
+
currentPassword: { type: String, required: true }, // ❌ Bắt buộc mật khẩu hiện tại
|
|
31
|
+
newPassword: { type: String, required: true }
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
3. **Service methods kiểm tra current password trong reset**:
|
|
36
|
+
```javascript
|
|
37
|
+
async resetPassword(userId, currentPassword, newPassword) {
|
|
38
|
+
const user = await User.findById(userId);
|
|
39
|
+
if (!user.validatePassword(currentPassword)) { // ❌ Validate mật khẩu hiện tại
|
|
40
|
+
throw new Error('Current password is incorrect');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
4. **React components với current password field**:
|
|
46
|
+
```typescript
|
|
47
|
+
function ResetPasswordForm() {
|
|
48
|
+
return (
|
|
49
|
+
<form>
|
|
50
|
+
<input name="currentPassword" required /> {/* ❌ Trường mật khẩu hiện tại */}
|
|
51
|
+
<input name="newPassword" required />
|
|
52
|
+
</form>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Cách khắc phục:
|
|
58
|
+
|
|
59
|
+
1. **Sử dụng token-based reset**:
|
|
60
|
+
```javascript
|
|
61
|
+
app.post('/reset-password', (req, res) => {
|
|
62
|
+
const { token, newPassword } = req.body; // ✅ Sử dụng token thay vì current password
|
|
63
|
+
if (!validateResetToken(token)) {
|
|
64
|
+
return res.status(400).json({ error: 'Invalid reset token' });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
2. **Schema với reset token**:
|
|
70
|
+
```typescript
|
|
71
|
+
const resetPasswordSchema = {
|
|
72
|
+
resetToken: { type: String, required: true }, // ✅ Token reset an toàn
|
|
73
|
+
newPassword: { type: String, required: true }
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
3. **Service method an toàn**:
|
|
78
|
+
```javascript
|
|
79
|
+
async resetPasswordWithToken(resetToken, newPassword) {
|
|
80
|
+
const tokenData = await validateResetToken(resetToken); // ✅ Validate token
|
|
81
|
+
if (!tokenData.valid) {
|
|
82
|
+
throw new Error('Invalid or expired reset token');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
4. **Form với email verification**:
|
|
88
|
+
```typescript
|
|
89
|
+
function ForgotPasswordForm() {
|
|
90
|
+
return (
|
|
91
|
+
<form>
|
|
92
|
+
<input name="email" type="email" required /> {/* ✅ Chỉ cần email */}
|
|
93
|
+
<button>Send Reset Link</button>
|
|
94
|
+
</form>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Tại sao đây là vấn đề bảo mật?
|
|
100
|
+
|
|
101
|
+
### 1. **Mâu thuẫn logic**
|
|
102
|
+
- Nếu người dùng nhớ mật khẩu hiện tại, họ không cần reset
|
|
103
|
+
- Yêu cầu mật khẩu hiện tại làm vô hiệu hóa tính năng "quên mật khẩu"
|
|
104
|
+
|
|
105
|
+
### 2. **Tạo điểm yếu bảo mật**
|
|
106
|
+
- Kẻ tấn công có thể lợi dụng để brute force mật khẩu
|
|
107
|
+
- Tăng surface attack cho account takeover
|
|
108
|
+
|
|
109
|
+
### 3. **Trải nghiệm người dùng kém**
|
|
110
|
+
- Người dùng quên mật khẩu không thể hoàn thành quy trình reset
|
|
111
|
+
- Dẫn đến khóa tài khoản và frustration
|
|
112
|
+
|
|
113
|
+
## Các trường hợp ngoại lệ
|
|
114
|
+
|
|
115
|
+
### Trường hợp hợp lệ (không phải lỗi):
|
|
116
|
+
|
|
117
|
+
1. **Password Change (không phải Reset)**:
|
|
118
|
+
```javascript
|
|
119
|
+
// ✅ Thay đổi mật khẩu khi đã đăng nhập - hợp lệ
|
|
120
|
+
app.post('/change-password', authenticateUser, (req, res) => {
|
|
121
|
+
const { currentPassword, newPassword } = req.body;
|
|
122
|
+
// Hợp lệ vì đây là thay đổi, không phải reset
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
2. **Profile settings**:
|
|
127
|
+
```javascript
|
|
128
|
+
// ✅ Cập nhật mật khẩu trong settings - hợp lệ
|
|
129
|
+
function ProfileSettings() {
|
|
130
|
+
return (
|
|
131
|
+
<div>
|
|
132
|
+
<h2>Change Password</h2> {/* Đây là change, không phải reset */}
|
|
133
|
+
<input name="currentPassword" />
|
|
134
|
+
<input name="newPassword" />
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Phương pháp detect
|
|
141
|
+
|
|
142
|
+
Rule này sử dụng **heuristic analysis** với các pattern:
|
|
143
|
+
|
|
144
|
+
1. **Context Detection**: Phát hiện ngữ cảnh password reset
|
|
145
|
+
- Keywords: `reset`, `forgot`, `recover`, `forgotpassword`
|
|
146
|
+
- Endpoints: `/reset-password`, `/forgot-password`
|
|
147
|
+
- Functions: `resetPassword()`, `forgotPassword()`
|
|
148
|
+
|
|
149
|
+
2. **Violation Detection**: Tìm yêu cầu current password
|
|
150
|
+
- Field names: `currentPassword`, `oldPassword`, `existingPassword`
|
|
151
|
+
- Validation patterns: `validateCurrentPassword()`, `checkOldPassword()`
|
|
152
|
+
- Schema fields: `currentPassword: { required: true }`
|
|
153
|
+
|
|
154
|
+
3. **Context Filtering**: Loại bỏ false positives
|
|
155
|
+
- Bỏ qua password change contexts
|
|
156
|
+
- Bỏ qua test files và documentation
|
|
157
|
+
- Bỏ qua comments và type definitions
|
|
158
|
+
|
|
159
|
+
## Tham khảo
|
|
160
|
+
|
|
161
|
+
- [OWASP A04:2021 - Insecure Design](https://owasp.org/Top10/A04_2021-Insecure_Design/)
|
|
162
|
+
- [CWE-640: Weak Password Recovery Mechanism for Forgotten Password](https://cwe.mitre.org/data/definitions/640.html)
|
|
163
|
+
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#forgot-password)
|
|
164
|
+
- [NIST SP 800-63B - Digital Identity Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html#sec5)
|
|
165
|
+
|
|
166
|
+
## Ví dụ
|
|
167
|
+
|
|
168
|
+
### Violation Examples
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
// ❌ Express.js với current password requirement
|
|
172
|
+
app.post('/reset-password', (req, res) => {
|
|
173
|
+
if (!req.body.currentPassword) {
|
|
174
|
+
return res.status(400).json({ error: 'Current password required' });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ❌ NestJS với validation current password
|
|
179
|
+
@Post('reset-password')
|
|
180
|
+
async resetPassword(@Body() data: { currentPassword: string, newPassword: string }) {
|
|
181
|
+
await this.authService.validateCurrentPassword(data.currentPassword);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ❌ Mongoose schema yêu cầu current password
|
|
185
|
+
const resetSchema = new Schema({
|
|
186
|
+
currentPassword: { type: String, required: true }, // Vi phạm
|
|
187
|
+
newPassword: { type: String, required: true }
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ❌ React form với current password field
|
|
191
|
+
<input
|
|
192
|
+
name="currentPassword"
|
|
193
|
+
placeholder="Enter current password"
|
|
194
|
+
required
|
|
195
|
+
/>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Secure Examples
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
// ✅ Token-based reset
|
|
202
|
+
app.post('/reset-password', (req, res) => {
|
|
203
|
+
const { token, newPassword } = req.body;
|
|
204
|
+
if (!validateResetToken(token)) {
|
|
205
|
+
return res.status(400).json({ error: 'Invalid reset token' });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ✅ Email-based forgot password
|
|
210
|
+
app.post('/forgot-password', (req, res) => {
|
|
211
|
+
const { email } = req.body;
|
|
212
|
+
sendResetEmail(email);
|
|
213
|
+
res.json({ message: 'Reset link sent to email' });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ✅ Secure reset schema
|
|
217
|
+
const resetSchema = new Schema({
|
|
218
|
+
resetToken: { type: String, required: true },
|
|
219
|
+
newPassword: { type: String, required: true },
|
|
220
|
+
tokenExpiry: { type: Date, required: true }
|
|
221
|
+
});
|
|
222
|
+
```
|