@sun-asterisk/sunlint 1.3.7 → 1.3.8
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 +38 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +190 -35
- package/core/file-targeting-service.js +83 -7
- package/package.json +1 -1
- package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
- package/rules/common/C065_one_behavior_per_test/config.json +95 -0
- package/rules/security/S037_cache_headers/README.md +128 -0
- package/rules/security/S037_cache_headers/analyzer.js +263 -0
- package/rules/security/S037_cache_headers/config.json +50 -0
- package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
- package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
- package/rules/security/S038_no_version_headers/README.md +234 -0
- package/rules/security/S038_no_version_headers/analyzer.js +262 -0
- package/rules/security/S038_no_version_headers/config.json +49 -0
- package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
- package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
- package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
- package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
- package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
- package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
- package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +436 -0
- package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
- package/rules/security/S049_short_validity_tokens/config.json +124 -0
- package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
- package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
- package/rules/security/S051_password_length_policy/analyzer.js +410 -0
- package/rules/security/S051_password_length_policy/config.json +83 -0
- package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
- package/rules/security/S052_weak_otp_entropy/config.json +57 -0
- package/rules/security/S054_no_default_accounts/README.md +129 -0
- package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
- package/rules/security/S054_no_default_accounts/config.json +101 -0
- package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
- package/rules/security/S056_log_injection_protection/config.json +148 -0
- package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
- package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +287 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S039 Regex-Based Analyzer - Do not pass Session Tokens via URL parameters
|
|
3
|
+
* Detects session token exposure in URL parameters across different frameworks
|
|
4
|
+
*/
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
|
|
7
|
+
class S039RegexBasedAnalyzer {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.ruleId = "S039";
|
|
10
|
+
|
|
11
|
+
// Framework-specific route patterns
|
|
12
|
+
this.routePatterns = {
|
|
13
|
+
// Express.js
|
|
14
|
+
express:
|
|
15
|
+
/\b(app|router)\.(get|post|put|delete|patch|use)\s*\(\s*['"`][^'"`]+['"`]/,
|
|
16
|
+
// Next.js API routes (legacy handler)
|
|
17
|
+
nextjs: /export\s+(default\s+)?async?\s+function\s+handler\s*\(/,
|
|
18
|
+
// Next.js 13+ App Router HTTP methods
|
|
19
|
+
nextjsApp:
|
|
20
|
+
/export\s+async?\s+function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(/,
|
|
21
|
+
// NestJS controllers
|
|
22
|
+
nestjs: /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`][^'"`]*['"`]?\s*\)/,
|
|
23
|
+
// Nuxt.js server routes
|
|
24
|
+
nuxtjs:
|
|
25
|
+
/export\s+(default\s+|const\s+(GET|POST|PUT|DELETE|PATCH)\s*=\s*)?defineEventHandler\s*\(/,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Session token parameter names to detect
|
|
29
|
+
this.sessionTokenParams = [
|
|
30
|
+
"sessionId",
|
|
31
|
+
"session_id",
|
|
32
|
+
"session-id",
|
|
33
|
+
"sessionToken",
|
|
34
|
+
"session_token",
|
|
35
|
+
"session-token",
|
|
36
|
+
"authToken",
|
|
37
|
+
"auth_token",
|
|
38
|
+
"auth-token",
|
|
39
|
+
"authorization",
|
|
40
|
+
"bearer",
|
|
41
|
+
"jwt",
|
|
42
|
+
"jwtToken",
|
|
43
|
+
"jwt_token",
|
|
44
|
+
"jwt-token",
|
|
45
|
+
"accessToken",
|
|
46
|
+
"access_token",
|
|
47
|
+
"access-token",
|
|
48
|
+
"refreshToken",
|
|
49
|
+
"refresh_token",
|
|
50
|
+
"refresh-token",
|
|
51
|
+
"apiKey",
|
|
52
|
+
"api_key",
|
|
53
|
+
"api-key",
|
|
54
|
+
"csrfToken",
|
|
55
|
+
"csrf_token",
|
|
56
|
+
"csrf-token",
|
|
57
|
+
"xsrfToken",
|
|
58
|
+
"xsrf_token",
|
|
59
|
+
"xsrf-token",
|
|
60
|
+
"token",
|
|
61
|
+
"apiToken",
|
|
62
|
+
"api_token",
|
|
63
|
+
"api-token",
|
|
64
|
+
"sid",
|
|
65
|
+
"sessionkey",
|
|
66
|
+
"session_key",
|
|
67
|
+
"session-key",
|
|
68
|
+
"userToken",
|
|
69
|
+
"user_token",
|
|
70
|
+
"user-token",
|
|
71
|
+
"authKey",
|
|
72
|
+
"auth_key",
|
|
73
|
+
"auth-key",
|
|
74
|
+
"securityToken",
|
|
75
|
+
"security_token",
|
|
76
|
+
"security-token",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// URL parameter access patterns for different frameworks
|
|
80
|
+
this.urlParamPatterns = {
|
|
81
|
+
// Express.js: req.query.sessionToken, req.params.authToken
|
|
82
|
+
express: /req\.(query|params)\.(\w+)/g,
|
|
83
|
+
// Express.js bracket notation: req.query["access-token"]
|
|
84
|
+
expressBracket: /req\.(query|params)\[['"`]([^'"`]+)['"`]\]/g,
|
|
85
|
+
// NestJS: @Query('sessionToken'), @Param('authToken')
|
|
86
|
+
nestjs: /@(Query|Param)\s*\(\s*['"`](\w+)['"`]\s*\)/g,
|
|
87
|
+
// Next.js: searchParams.get('sessionToken')
|
|
88
|
+
nextjs: /searchParams\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g,
|
|
89
|
+
// Generic destructuring: { sessionToken } = req.query
|
|
90
|
+
destructuring: /\{\s*([^}]+)\s*\}\s*=\s*req\.(query|params)/g,
|
|
91
|
+
// URL constructor patterns: new URL().searchParams.get()
|
|
92
|
+
urlConstructor:
|
|
93
|
+
/new\s+URL\([^)]+\)\.searchParams\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g,
|
|
94
|
+
// URLSearchParams patterns: params.get("token")
|
|
95
|
+
urlSearchParams: /(\w+)\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Pattern to detect session token-like values
|
|
99
|
+
this.tokenValuePattern = /^[a-zA-Z0-9+/=\-_.]{16,}$/;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async analyze(filePath) {
|
|
103
|
+
// Skip files that are unlikely to be route handlers
|
|
104
|
+
const skipPatterns = [
|
|
105
|
+
/\.dto\.ts$/,
|
|
106
|
+
/\.interface\.ts$/,
|
|
107
|
+
/\.module\.ts$/,
|
|
108
|
+
/\.service\.spec\.ts$/,
|
|
109
|
+
/\.controller\.spec\.ts$/,
|
|
110
|
+
/\.spec\.ts$/,
|
|
111
|
+
/\.test\.ts$/,
|
|
112
|
+
/\.d\.ts$/,
|
|
113
|
+
/\.types\.ts$/,
|
|
114
|
+
/\.constants?\.ts$/,
|
|
115
|
+
/\.config\.ts$/,
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
|
|
119
|
+
if (shouldSkip) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
124
|
+
const lines = content.split(/\r?\n/);
|
|
125
|
+
const violations = [];
|
|
126
|
+
|
|
127
|
+
let inRoute = false;
|
|
128
|
+
let braceDepth = 0;
|
|
129
|
+
let routeStartLine = 0;
|
|
130
|
+
let routeType = null;
|
|
131
|
+
const tokenExposures = [];
|
|
132
|
+
|
|
133
|
+
// Helper functions
|
|
134
|
+
const reset = () => {
|
|
135
|
+
inRoute = false;
|
|
136
|
+
braceDepth = 0;
|
|
137
|
+
routeStartLine = 0;
|
|
138
|
+
routeType = null;
|
|
139
|
+
tokenExposures.length = 0;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const evaluate = () => {
|
|
143
|
+
// Report violations for exposed session tokens
|
|
144
|
+
for (const exposure of tokenExposures) {
|
|
145
|
+
violations.push({
|
|
146
|
+
ruleId: this.ruleId,
|
|
147
|
+
message: `Session token '${exposure.paramName}' passed via URL parameter - use secure headers or request body instead`,
|
|
148
|
+
severity: "warning",
|
|
149
|
+
line: exposure.line,
|
|
150
|
+
column: 1,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
156
|
+
const line = lines[i];
|
|
157
|
+
|
|
158
|
+
// Detect route start for different frameworks
|
|
159
|
+
if (!inRoute) {
|
|
160
|
+
for (const [framework, pattern] of Object.entries(this.routePatterns)) {
|
|
161
|
+
if (pattern.test(line)) {
|
|
162
|
+
if (process.env.SUNLINT_DEBUG)
|
|
163
|
+
console.log(
|
|
164
|
+
`🔧 [S039-Regex] Found ${framework} route at line ${
|
|
165
|
+
i + 1
|
|
166
|
+
}: ${line.trim()}`
|
|
167
|
+
);
|
|
168
|
+
inRoute = true;
|
|
169
|
+
routeStartLine = i + 1;
|
|
170
|
+
routeType = framework;
|
|
171
|
+
braceDepth =
|
|
172
|
+
(line.match(/\{/g) || []).length -
|
|
173
|
+
(line.match(/\}/g) || []).length;
|
|
174
|
+
|
|
175
|
+
// If no opening brace on this line, look ahead for it
|
|
176
|
+
if (braceDepth === 0) {
|
|
177
|
+
for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
|
|
178
|
+
const nextLine = lines[j];
|
|
179
|
+
if (nextLine.includes("{")) {
|
|
180
|
+
braceDepth =
|
|
181
|
+
(nextLine.match(/\{/g) || []).length -
|
|
182
|
+
(nextLine.match(/\}/g) || []).length;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (inRoute) {
|
|
193
|
+
// Update brace depth
|
|
194
|
+
braceDepth +=
|
|
195
|
+
(line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
196
|
+
|
|
197
|
+
// Check for session token parameter access
|
|
198
|
+
this.checkTokenParameterAccess(line, i + 1, tokenExposures);
|
|
199
|
+
|
|
200
|
+
// End of route detection
|
|
201
|
+
if (
|
|
202
|
+
braceDepth <= 0 &&
|
|
203
|
+
(/\)\s*;?\s*$/.test(line) ||
|
|
204
|
+
/^\s*\}\s*$/.test(line) ||
|
|
205
|
+
/^export/.test(line))
|
|
206
|
+
) {
|
|
207
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
208
|
+
console.log(
|
|
209
|
+
`🔧 [S039-Regex] Route ended, evaluating: Token exposures=${tokenExposures.length}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
evaluate();
|
|
213
|
+
reset();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Safety evaluate if unbalanced at file end
|
|
219
|
+
if (inRoute) evaluate();
|
|
220
|
+
|
|
221
|
+
return violations;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
checkTokenParameterAccess(line, lineNumber, exposures) {
|
|
225
|
+
// Check each URL parameter access pattern
|
|
226
|
+
for (const [framework, pattern] of Object.entries(this.urlParamPatterns)) {
|
|
227
|
+
const matches = [...line.matchAll(pattern)];
|
|
228
|
+
|
|
229
|
+
for (const match of matches) {
|
|
230
|
+
let paramName = null;
|
|
231
|
+
|
|
232
|
+
if (framework === "express") {
|
|
233
|
+
// req.query.sessionToken or req.params.authToken
|
|
234
|
+
paramName = match[2];
|
|
235
|
+
} else if (framework === "expressBracket") {
|
|
236
|
+
// req.query["access-token"] or req.params["auth-token"]
|
|
237
|
+
paramName = match[2];
|
|
238
|
+
} else if (framework === "nestjs") {
|
|
239
|
+
// @Query('sessionToken') or @Param('authToken')
|
|
240
|
+
paramName = match[2];
|
|
241
|
+
} else if (framework === "nextjs" || framework === "urlConstructor") {
|
|
242
|
+
// searchParams.get('sessionToken'), new URL().searchParams.get('token')
|
|
243
|
+
paramName = match[1];
|
|
244
|
+
} else if (framework === "urlSearchParams") {
|
|
245
|
+
// params.get('sessionToken') - general URLSearchParams pattern
|
|
246
|
+
paramName = match[2];
|
|
247
|
+
} else if (framework === "destructuring") {
|
|
248
|
+
// { sessionToken, authToken } = req.query
|
|
249
|
+
const destructuredParams = match[1].split(",").map((p) => p.trim());
|
|
250
|
+
for (const param of destructuredParams) {
|
|
251
|
+
const cleanParam = param.replace(/['"]/g, "");
|
|
252
|
+
if (this.isSessionTokenParam(cleanParam)) {
|
|
253
|
+
exposures.push({
|
|
254
|
+
paramName: cleanParam,
|
|
255
|
+
line: lineNumber,
|
|
256
|
+
framework: framework,
|
|
257
|
+
accessType: match[2], // query or params
|
|
258
|
+
});
|
|
259
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
260
|
+
console.log(
|
|
261
|
+
`🔧 [S039-Regex] Found token parameter exposure: ${cleanParam} via ${framework}`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
continue; // Skip the normal parameter check below
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (paramName && this.isSessionTokenParam(paramName)) {
|
|
270
|
+
exposures.push({
|
|
271
|
+
paramName: paramName,
|
|
272
|
+
line: lineNumber,
|
|
273
|
+
framework: framework,
|
|
274
|
+
accessType: framework === "express" ? match[1] : "parameter",
|
|
275
|
+
});
|
|
276
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
277
|
+
console.log(
|
|
278
|
+
`🔧 [S039-Regex] Found token parameter exposure: ${paramName} via ${framework}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Additional patterns for hardcoded URL parsing
|
|
286
|
+
const hardcodedPatterns = [
|
|
287
|
+
// window.location.search, location.search
|
|
288
|
+
/(?:window\.)?location\.search/g,
|
|
289
|
+
// URLSearchParams(location.search)
|
|
290
|
+
/URLSearchParams\s*\(\s*(?:window\.)?location\.search\s*\)/g,
|
|
291
|
+
// document.location.search
|
|
292
|
+
/document\.location\.search/g,
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
for (const pattern of hardcodedPatterns) {
|
|
296
|
+
if (pattern.test(line)) {
|
|
297
|
+
// Look for subsequent .get() calls or parameter access in nearby lines
|
|
298
|
+
for (
|
|
299
|
+
let j = Math.max(0, lineNumber - 3);
|
|
300
|
+
j < Math.min(lineNumber + 3, exposures.length + lineNumber);
|
|
301
|
+
j++
|
|
302
|
+
) {
|
|
303
|
+
const nearbyLine = exposures[j] || line;
|
|
304
|
+
const getMatches = [
|
|
305
|
+
...nearbyLine.matchAll(/\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g),
|
|
306
|
+
];
|
|
307
|
+
for (const getMatch of getMatches) {
|
|
308
|
+
const paramName = getMatch[1];
|
|
309
|
+
if (this.isSessionTokenParam(paramName)) {
|
|
310
|
+
exposures.push({
|
|
311
|
+
paramName: paramName,
|
|
312
|
+
line: lineNumber,
|
|
313
|
+
framework: "client-side",
|
|
314
|
+
accessType: "url-search",
|
|
315
|
+
});
|
|
316
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
317
|
+
console.log(
|
|
318
|
+
`🔧 [S039-Regex] Found client-side token parameter: ${paramName}`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
isSessionTokenParam(paramName) {
|
|
329
|
+
return this.sessionTokenParams.some(
|
|
330
|
+
(tokenParam) => tokenParam.toLowerCase() === paramName.toLowerCase()
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
cleanup() {}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = S039RegexBasedAnalyzer;
|