@sun-asterisk/sunlint 1.3.2 → 1.3.4
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 +73 -0
- package/README.md +5 -3
- package/config/rules/enhanced-rules-registry.json +144 -33
- package/core/analysis-orchestrator.js +173 -42
- package/core/auto-performance-manager.js +243 -0
- package/core/cli-action-handler.js +24 -2
- package/core/cli-program.js +19 -5
- package/core/constants/defaults.js +56 -0
- package/core/performance-optimizer.js +271 -0
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +151 -0
- package/docs/FILE_LIMITS_EXPLANATION.md +190 -0
- package/docs/PERFORMANCE.md +311 -0
- package/docs/PERFORMANCE_MIGRATION_GUIDE.md +368 -0
- package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +255 -0
- package/docs/QUICK_FILE_LIMITS.md +64 -0
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +208 -0
- package/engines/engine-factory.js +7 -0
- package/engines/heuristic-engine.js +182 -5
- package/package.json +2 -1
- package/rules/common/C048_no_bypass_architectural_layers/analyzer.js +180 -0
- package/rules/common/C048_no_bypass_architectural_layers/config.json +50 -0
- package/rules/common/C048_no_bypass_architectural_layers/symbol-based-analyzer.js +235 -0
- package/rules/common/C052_parsing_or_data_transformation/analyzer.js +180 -0
- package/rules/common/C052_parsing_or_data_transformation/config.json +50 -0
- package/rules/common/C052_parsing_or_data_transformation/symbol-based-analyzer.js +132 -0
- package/rules/index.js +2 -0
- package/rules/security/S017_use_parameterized_queries/README.md +128 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +286 -0
- package/rules/security/S017_use_parameterized_queries/config.json +109 -0
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +541 -0
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +777 -0
- package/rules/security/S031_secure_session_cookies/README.md +127 -0
- package/rules/security/S031_secure_session_cookies/analyzer.js +245 -0
- package/rules/security/S031_secure_session_cookies/config.json +86 -0
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +196 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +1084 -0
- package/rules/security/S032_httponly_session_cookies/FRAMEWORK_SUPPORT.md +209 -0
- package/rules/security/S032_httponly_session_cookies/README.md +184 -0
- package/rules/security/S032_httponly_session_cookies/analyzer.js +282 -0
- package/rules/security/S032_httponly_session_cookies/config.json +96 -0
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +715 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +1348 -0
- package/rules/security/S033_samesite_session_cookies/README.md +227 -0
- package/rules/security/S033_samesite_session_cookies/analyzer.js +242 -0
- package/rules/security/S033_samesite_session_cookies/config.json +87 -0
- package/rules/security/S033_samesite_session_cookies/regex-based-analyzer.js +703 -0
- package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +732 -0
- package/rules/security/S034_host_prefix_session_cookies/README.md +204 -0
- package/rules/security/S034_host_prefix_session_cookies/analyzer.js +290 -0
- package/rules/security/S034_host_prefix_session_cookies/config.json +62 -0
- package/rules/security/S034_host_prefix_session_cookies/regex-based-analyzer.js +478 -0
- package/rules/security/S034_host_prefix_session_cookies/symbol-based-analyzer.js +277 -0
- package/rules/security/S035_path_session_cookies/README.md +257 -0
- package/rules/security/S035_path_session_cookies/analyzer.js +316 -0
- package/rules/security/S035_path_session_cookies/config.json +99 -0
- package/rules/security/S035_path_session_cookies/regex-based-analyzer.js +724 -0
- package/rules/security/S035_path_session_cookies/symbol-based-analyzer.js +373 -0
- package/scripts/batch-processing-demo.js +334 -0
- package/scripts/performance-test.js +541 -0
- package/scripts/quick-performance-test.js +108 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S032 Regex-Based Analyzer - Set HttpOnly attribute for Session Cookies
|
|
3
|
+
* Fallback analysis using regex patterns
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
|
|
8
|
+
class S032RegexBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.semanticEngine = semanticEngine;
|
|
11
|
+
this.ruleId = "S032";
|
|
12
|
+
this.category = "security";
|
|
13
|
+
|
|
14
|
+
// Session cookie indicators
|
|
15
|
+
this.sessionIndicators = [
|
|
16
|
+
"session",
|
|
17
|
+
"sessionid",
|
|
18
|
+
"sessid",
|
|
19
|
+
"jsessionid",
|
|
20
|
+
"phpsessid",
|
|
21
|
+
"asp.net_sessionid",
|
|
22
|
+
"connect.sid",
|
|
23
|
+
"auth",
|
|
24
|
+
"token",
|
|
25
|
+
"jwt",
|
|
26
|
+
"csrf",
|
|
27
|
+
"refresh",
|
|
28
|
+
// NestJS specific
|
|
29
|
+
"nest-session",
|
|
30
|
+
"nest-auth",
|
|
31
|
+
// Next.js specific
|
|
32
|
+
"next-auth.session-token",
|
|
33
|
+
"next-auth.csrf-token",
|
|
34
|
+
"__Host-next-auth.csrf-token",
|
|
35
|
+
"__Secure-next-auth.session-token",
|
|
36
|
+
// Nuxt.js specific
|
|
37
|
+
"nuxt-session",
|
|
38
|
+
"nuxt-auth",
|
|
39
|
+
"auth._token",
|
|
40
|
+
"auth._refresh_token",
|
|
41
|
+
// General framework patterns
|
|
42
|
+
"access_token",
|
|
43
|
+
"refresh_token",
|
|
44
|
+
"id_token",
|
|
45
|
+
"state_token",
|
|
46
|
+
"nonce",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Regex patterns for cookie detection
|
|
50
|
+
this.cookiePatterns = [
|
|
51
|
+
// Express/Node.js cookie patterns
|
|
52
|
+
/res\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
|
|
53
|
+
/response\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
|
|
54
|
+
|
|
55
|
+
// NestJS specific patterns (more specific to avoid overlap)
|
|
56
|
+
/@Res\(\)\s*\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
|
|
57
|
+
|
|
58
|
+
// Next.js patterns
|
|
59
|
+
/NextResponse\.next\(\)\.cookies\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
|
|
60
|
+
/cookies\(\)\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
|
|
61
|
+
/\.cookies\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
|
|
62
|
+
|
|
63
|
+
// Nuxt.js patterns
|
|
64
|
+
/useCookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*({[^}]+})/gi,
|
|
65
|
+
/\$cookies\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
|
|
66
|
+
|
|
67
|
+
// Set-Cookie header patterns (array format)
|
|
68
|
+
/setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*\[\s*([^\]]+)\s*\]/gi,
|
|
69
|
+
|
|
70
|
+
// Set-Cookie header patterns (single string)
|
|
71
|
+
/setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*['"`]([^'"`]+)['"`]/gi,
|
|
72
|
+
|
|
73
|
+
// Session middleware patterns
|
|
74
|
+
/session\s*\(\s*({[^}]+})/gi,
|
|
75
|
+
/\.use\s*\(\s*session\s*\(\s*({[^}]+})/gi,
|
|
76
|
+
|
|
77
|
+
// Framework-specific session patterns
|
|
78
|
+
/NextAuth\s*\(\s*({[^}]+})/gi,
|
|
79
|
+
|
|
80
|
+
// Generic cookie method (only if not caught by above patterns)
|
|
81
|
+
/(?<!response\.)(?<!res\.)(?<!@Res\(\)\s*)\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// NextAuth configuration patterns
|
|
85
|
+
this.nextAuthPatterns = [
|
|
86
|
+
// Individual cookie configuration patterns for sessionToken
|
|
87
|
+
/sessionToken\s*:\s*\{[^{]*?name\s*:\s*['"`]([^'"`]+)['"`][^{]*?options\s*:\s*\{([^}]+)\}/gis,
|
|
88
|
+
|
|
89
|
+
// Individual cookie configuration patterns for csrfToken
|
|
90
|
+
/csrfToken\s*:\s*\{[^{]*?name\s*:\s*['"`]([^'"`]+)['"`][^{]*?options\s*:\s*\{([^}]+)\}/gis,
|
|
91
|
+
|
|
92
|
+
// Generic cookie configuration pattern (fallback)
|
|
93
|
+
/(\w+Token)\s*:\s*\{[^{]*?name\s*:\s*['"`]([^'"`]+)['"`][^{]*?options\s*:\s*\{([^}]+)\}/gis,
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Initialize analyzer
|
|
99
|
+
*/
|
|
100
|
+
async initialize(semanticEngine) {
|
|
101
|
+
this.semanticEngine = semanticEngine;
|
|
102
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
103
|
+
console.log(`🔧 [S032] Regex-based analyzer initialized`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Main analysis method
|
|
109
|
+
*/
|
|
110
|
+
async analyze(filePath) {
|
|
111
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
112
|
+
console.log(`🔍 [S032] Regex-based analysis for: ${filePath}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const violations = [];
|
|
116
|
+
const violationMap = new Map(); // Deduplication map
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
120
|
+
const lines = content.split("\n");
|
|
121
|
+
|
|
122
|
+
// Check each pattern
|
|
123
|
+
for (const pattern of this.cookiePatterns) {
|
|
124
|
+
this.checkPattern(pattern, content, lines, violations, filePath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check NextAuth configuration patterns
|
|
128
|
+
for (const pattern of this.nextAuthPatterns) {
|
|
129
|
+
this.checkNextAuthPattern(
|
|
130
|
+
pattern,
|
|
131
|
+
content,
|
|
132
|
+
lines,
|
|
133
|
+
violations,
|
|
134
|
+
filePath
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for session middleware without cookie config
|
|
139
|
+
// This method is now mainly handled by checkPattern, but keep for edge cases
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.warn(
|
|
142
|
+
`⚠ [S032] Regex analysis failed for ${filePath}:`,
|
|
143
|
+
error.message
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Deduplicate violations based on line, column, and message
|
|
148
|
+
violations.forEach((violation) => {
|
|
149
|
+
const key = `${violation.line}:${violation.column}:${violation.message}`;
|
|
150
|
+
if (!violationMap.has(key)) {
|
|
151
|
+
violationMap.set(key, violation);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const uniqueViolations = Array.from(violationMap.values());
|
|
156
|
+
|
|
157
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
158
|
+
console.log(
|
|
159
|
+
`🔧 [S032] Regex analysis completed: ${violations.length} total, ${uniqueViolations.length} unique violations`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return uniqueViolations;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check a specific regex pattern for violations
|
|
168
|
+
*/
|
|
169
|
+
checkPattern(pattern, content, lines, violations, filePath) {
|
|
170
|
+
pattern.lastIndex = 0; // Reset regex state
|
|
171
|
+
let match;
|
|
172
|
+
|
|
173
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
174
|
+
const matchText = match[0];
|
|
175
|
+
const cookieName = match[1] || "";
|
|
176
|
+
const cookieConfig = match[2] || match[1] || matchText;
|
|
177
|
+
|
|
178
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
179
|
+
console.log(
|
|
180
|
+
`🔍 [S032] Regex: Pattern match - cookieName: "${cookieName}", config: "${cookieConfig.substring(
|
|
181
|
+
0,
|
|
182
|
+
50
|
|
183
|
+
)}..."`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Handle different patterns
|
|
188
|
+
if (matchText.includes("setHeader") && matchText.includes("Set-Cookie")) {
|
|
189
|
+
this.checkSetCookieHeaderPattern(match, content, violations, filePath);
|
|
190
|
+
} else if (matchText.includes("session(")) {
|
|
191
|
+
this.checkSessionMiddlewarePattern(
|
|
192
|
+
match,
|
|
193
|
+
content,
|
|
194
|
+
violations,
|
|
195
|
+
filePath
|
|
196
|
+
);
|
|
197
|
+
} else if (
|
|
198
|
+
matchText.includes("NextAuth(") ||
|
|
199
|
+
matchText.includes("useCookie(")
|
|
200
|
+
) {
|
|
201
|
+
this.checkFrameworkSpecificPattern(
|
|
202
|
+
match,
|
|
203
|
+
content,
|
|
204
|
+
violations,
|
|
205
|
+
filePath
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
// Regular cookie patterns
|
|
209
|
+
if (this.isSessionCookie(cookieName, matchText)) {
|
|
210
|
+
if (!this.hasHttpOnlyFlag(cookieConfig)) {
|
|
211
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
212
|
+
|
|
213
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
214
|
+
console.log(
|
|
215
|
+
`🔍 [S032] Regex: ⚠️ VIOLATION FOUND: Line ${lineNumber} - "${cookieName}" missing httpOnly`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
violations.push({
|
|
220
|
+
rule: this.ruleId,
|
|
221
|
+
source: filePath,
|
|
222
|
+
category: this.category,
|
|
223
|
+
line: lineNumber,
|
|
224
|
+
column: 1,
|
|
225
|
+
message: `Insecure session cookie: Session cookie "${cookieName}" missing HttpOnly attribute`,
|
|
226
|
+
severity: "error",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check NextAuth configuration patterns for missing httpOnly
|
|
236
|
+
*/
|
|
237
|
+
checkNextAuthPattern(pattern, content, lines, violations, filePath) {
|
|
238
|
+
pattern.lastIndex = 0; // Reset regex state
|
|
239
|
+
let match;
|
|
240
|
+
|
|
241
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
242
|
+
const matchText = match[0];
|
|
243
|
+
|
|
244
|
+
// Handle different pattern capture groups
|
|
245
|
+
let cookieName, optionsConfig;
|
|
246
|
+
if (match[3]) {
|
|
247
|
+
// Generic pattern: match[1] = tokenType, match[2] = name, match[3] = options
|
|
248
|
+
cookieName = match[2];
|
|
249
|
+
optionsConfig = match[3];
|
|
250
|
+
} else {
|
|
251
|
+
// Specific patterns: match[1] = name, match[2] = options
|
|
252
|
+
cookieName = match[1] || "session-cookie";
|
|
253
|
+
optionsConfig = match[2] || "";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
257
|
+
console.log(
|
|
258
|
+
`🔍 [S032] NextAuth: Pattern match - cookieName: "${cookieName}", options: "${optionsConfig.substring(
|
|
259
|
+
0,
|
|
260
|
+
50
|
|
261
|
+
)}..."`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check if httpOnly is missing or false
|
|
266
|
+
if (!this.hasHttpOnlyTrue(optionsConfig)) {
|
|
267
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
268
|
+
|
|
269
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
270
|
+
console.log(
|
|
271
|
+
`❌ [S032] NextAuth: Missing HttpOnly for cookie "${cookieName}" at line ${lineNumber}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
violations.push({
|
|
276
|
+
rule: this.ruleId,
|
|
277
|
+
source: filePath,
|
|
278
|
+
category: this.category,
|
|
279
|
+
line: lineNumber,
|
|
280
|
+
column: 1,
|
|
281
|
+
message: `NextAuth session cookie: Cookie "${cookieName}" missing HttpOnly attribute in authOptions configuration`,
|
|
282
|
+
severity: "error",
|
|
283
|
+
});
|
|
284
|
+
} else {
|
|
285
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
286
|
+
console.log(
|
|
287
|
+
`✅ [S032] NextAuth: Cookie "${cookieName}" has HttpOnly set correctly`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Check Set-Cookie header patterns
|
|
296
|
+
*/
|
|
297
|
+
checkSetCookieHeaderPattern(match, content, violations, filePath) {
|
|
298
|
+
const matchText = match[0];
|
|
299
|
+
|
|
300
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
301
|
+
console.log(
|
|
302
|
+
`🔍 [S032] Regex: Checking Set-Cookie header pattern: ${matchText.substring(
|
|
303
|
+
0,
|
|
304
|
+
100
|
|
305
|
+
)}...`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Extract cookie strings from setHeader array format
|
|
310
|
+
if (matchText.includes("[")) {
|
|
311
|
+
const arrayMatch = matchText.match(/\[\s*([^\]]+)\s*\]/);
|
|
312
|
+
if (arrayMatch) {
|
|
313
|
+
const cookiesContent = arrayMatch[1];
|
|
314
|
+
|
|
315
|
+
// Split by comma but preserve template literals
|
|
316
|
+
const cookieStrings = this.splitCookieStrings(cookiesContent);
|
|
317
|
+
|
|
318
|
+
for (const cookieString of cookieStrings) {
|
|
319
|
+
const cookieName = this.extractCookieNameFromString(cookieString);
|
|
320
|
+
|
|
321
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
322
|
+
console.log(
|
|
323
|
+
`🔍 [S032] Regex: Checking Set-Cookie string: "${cookieString.substring(
|
|
324
|
+
0,
|
|
325
|
+
50
|
|
326
|
+
)}..." - name: "${cookieName}"`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (this.isSessionCookie(cookieName, cookieString)) {
|
|
331
|
+
const hasHttpOnly = cookieString.toLowerCase().includes("httponly");
|
|
332
|
+
|
|
333
|
+
if (!hasHttpOnly) {
|
|
334
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
335
|
+
|
|
336
|
+
violations.push({
|
|
337
|
+
rule: this.ruleId,
|
|
338
|
+
source: filePath,
|
|
339
|
+
category: this.category,
|
|
340
|
+
line: lineNumber,
|
|
341
|
+
column: 1,
|
|
342
|
+
message: `Insecure session cookie: Session cookie "${cookieName}" in Set-Cookie header missing HttpOnly attribute`,
|
|
343
|
+
severity: "error",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Split cookie strings while preserving template literals
|
|
354
|
+
*/
|
|
355
|
+
splitCookieStrings(cookiesContent) {
|
|
356
|
+
const cookieStrings = [];
|
|
357
|
+
let current = "";
|
|
358
|
+
let inTemplate = false;
|
|
359
|
+
let quoteChar = null;
|
|
360
|
+
|
|
361
|
+
for (let i = 0; i < cookiesContent.length; i++) {
|
|
362
|
+
const char = cookiesContent[i];
|
|
363
|
+
|
|
364
|
+
if ((char === '"' || char === "'" || char === "`") && !quoteChar) {
|
|
365
|
+
quoteChar = char;
|
|
366
|
+
current += char;
|
|
367
|
+
} else if (char === quoteChar) {
|
|
368
|
+
quoteChar = null;
|
|
369
|
+
current += char;
|
|
370
|
+
} else if (char === "," && !quoteChar) {
|
|
371
|
+
if (current.trim()) {
|
|
372
|
+
cookieStrings.push(current.trim());
|
|
373
|
+
current = "";
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
current += char;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (current.trim()) {
|
|
381
|
+
cookieStrings.push(current.trim());
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return cookieStrings;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Extract cookie name from string like "auth=${tokens.auth}; ..." or `auth=${value}; ...`
|
|
389
|
+
*/
|
|
390
|
+
extractCookieNameFromString(cookieString) {
|
|
391
|
+
try {
|
|
392
|
+
// Remove quotes and backticks
|
|
393
|
+
const cleaned = cookieString
|
|
394
|
+
.replace(/^[`'"]/g, "")
|
|
395
|
+
.replace(/[`'"]$/g, "");
|
|
396
|
+
|
|
397
|
+
// Match cookie name before = sign
|
|
398
|
+
const match = cleaned.match(/^(\w+)=/);
|
|
399
|
+
return match ? match[1] : null;
|
|
400
|
+
} catch (error) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Check session middleware pattern
|
|
407
|
+
*/
|
|
408
|
+
checkSessionMiddlewarePattern(match, content, violations, filePath) {
|
|
409
|
+
const sessionConfig = match[1];
|
|
410
|
+
|
|
411
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
412
|
+
console.log(
|
|
413
|
+
`🔍 [S032] Regex: Checking session middleware: ${sessionConfig.substring(
|
|
414
|
+
0,
|
|
415
|
+
100
|
|
416
|
+
)}...`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Check if session has cookie configuration
|
|
421
|
+
if (sessionConfig.includes("cookie:")) {
|
|
422
|
+
// Has cookie config, check for httpOnly
|
|
423
|
+
if (!this.hasHttpOnlyFlag(sessionConfig)) {
|
|
424
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
425
|
+
|
|
426
|
+
violations.push({
|
|
427
|
+
rule: this.ruleId,
|
|
428
|
+
source: filePath,
|
|
429
|
+
category: this.category,
|
|
430
|
+
line: lineNumber,
|
|
431
|
+
column: 1,
|
|
432
|
+
message:
|
|
433
|
+
"Insecure session cookie: Session middleware missing httpOnly attribute",
|
|
434
|
+
severity: "error",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
// No cookie config at all
|
|
439
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
440
|
+
|
|
441
|
+
violations.push({
|
|
442
|
+
rule: this.ruleId,
|
|
443
|
+
source: filePath,
|
|
444
|
+
category: this.category,
|
|
445
|
+
line: lineNumber,
|
|
446
|
+
column: 1,
|
|
447
|
+
message:
|
|
448
|
+
"Insecure session cookie: Session middleware missing cookie configuration with httpOnly",
|
|
449
|
+
severity: "error",
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Check if cookie name indicates session cookie
|
|
456
|
+
*/
|
|
457
|
+
isSessionCookie(cookieName, fullMatch) {
|
|
458
|
+
if (!cookieName && fullMatch.includes("session")) {
|
|
459
|
+
return true; // Session middleware
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!cookieName) return false;
|
|
463
|
+
|
|
464
|
+
const lowerName = cookieName.toLowerCase();
|
|
465
|
+
return this.sessionIndicators.some((indicator) =>
|
|
466
|
+
lowerName.includes(indicator.toLowerCase())
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Check if configuration has httpOnly flag
|
|
472
|
+
*/
|
|
473
|
+
hasHttpOnlyFlag(configText) {
|
|
474
|
+
// Skip if this is a reference to external config
|
|
475
|
+
if (this.isConfigReference(configText)) {
|
|
476
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
477
|
+
console.log(
|
|
478
|
+
`🔍 [S032] Regex: Skipping config reference: ${configText.substring(
|
|
479
|
+
0,
|
|
480
|
+
30
|
|
481
|
+
)}...`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return true; // Assume external config is secure (avoid false positives)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Remove comments to avoid false positives from "// Missing: httpOnly: true"
|
|
488
|
+
const codeOnly = configText
|
|
489
|
+
.replace(/\/\/.*$/gm, "")
|
|
490
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
491
|
+
|
|
492
|
+
const httpOnlyPatterns = [
|
|
493
|
+
/httpOnly\s*:\s*true/i,
|
|
494
|
+
/httpOnly\s*=\s*true/i,
|
|
495
|
+
/['"]httpOnly['"]\s*:\s*true/i,
|
|
496
|
+
/HttpOnly/i, // For Set-Cookie header format
|
|
497
|
+
];
|
|
498
|
+
|
|
499
|
+
// Check for explicitly disabled httpOnly (should be treated as violation)
|
|
500
|
+
const httpOnlyDisabledPatterns = [
|
|
501
|
+
/httpOnly\s*:\s*false/i,
|
|
502
|
+
/httpOnly\s*=\s*false/i,
|
|
503
|
+
/['"]httpOnly['"]\s*:\s*false/i,
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
const hasHttpOnlyFalse = httpOnlyDisabledPatterns.some((pattern) =>
|
|
507
|
+
pattern.test(codeOnly)
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
const hasHttpOnly = httpOnlyPatterns.some((pattern) =>
|
|
511
|
+
pattern.test(codeOnly)
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
// If httpOnly is explicitly set to false, it's a violation
|
|
515
|
+
if (hasHttpOnlyFalse) {
|
|
516
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
517
|
+
console.log(
|
|
518
|
+
`🔍 [S032] Regex: HttpOnly explicitly disabled (violation): ${configText.substring(
|
|
519
|
+
0,
|
|
520
|
+
50
|
|
521
|
+
)}...`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
return false; // Violation: explicitly disabled
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
528
|
+
console.log(
|
|
529
|
+
`🔍 [S032] Regex: HttpOnly check result: ${hasHttpOnly} for config (without comments): ${codeOnly.substring(
|
|
530
|
+
0,
|
|
531
|
+
50
|
|
532
|
+
)}...`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return hasHttpOnly;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Check if the config text is a reference to external configuration
|
|
541
|
+
*/
|
|
542
|
+
isConfigReference(configText) {
|
|
543
|
+
const refPatterns = [
|
|
544
|
+
/this\.\w+/, // this.cookieConfig
|
|
545
|
+
/\w+Config/, // someConfig
|
|
546
|
+
/\.\.\.\w+/, // ...spread
|
|
547
|
+
/\w+\.\w+/, // object.property
|
|
548
|
+
];
|
|
549
|
+
|
|
550
|
+
return refPatterns.some((pattern) => pattern.test(configText.trim()));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Get line number from character index
|
|
555
|
+
*/
|
|
556
|
+
getLineNumber(content, index) {
|
|
557
|
+
const beforeMatch = content.substring(0, index);
|
|
558
|
+
return beforeMatch.split("\n").length;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Check framework-specific patterns (Next.js, Nuxt.js, etc.)
|
|
563
|
+
*/
|
|
564
|
+
checkFrameworkSpecificPattern(match, content, violations, filePath) {
|
|
565
|
+
const matchText = match[0];
|
|
566
|
+
const cookieName = match[1] || "";
|
|
567
|
+
const cookieConfig = match[2] || match[1] || "";
|
|
568
|
+
|
|
569
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
570
|
+
console.log(
|
|
571
|
+
`🔍 [S032] Regex: Checking framework-specific pattern: ${matchText.substring(
|
|
572
|
+
0,
|
|
573
|
+
100
|
|
574
|
+
)}...`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Handle NextAuth patterns
|
|
579
|
+
if (matchText.includes("NextAuth(")) {
|
|
580
|
+
if (!this.hasHttpOnlyInNextAuthConfig(cookieConfig)) {
|
|
581
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
582
|
+
violations.push({
|
|
583
|
+
rule: this.ruleId,
|
|
584
|
+
source: filePath,
|
|
585
|
+
category: this.category,
|
|
586
|
+
line: lineNumber,
|
|
587
|
+
column: 1,
|
|
588
|
+
message:
|
|
589
|
+
"Insecure session cookie: NextAuth configuration missing httpOnly for session cookies",
|
|
590
|
+
severity: "error",
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Handle Nuxt useCookie patterns
|
|
597
|
+
if (matchText.includes("useCookie(")) {
|
|
598
|
+
if (
|
|
599
|
+
this.isSessionCookie(cookieName, matchText) &&
|
|
600
|
+
!this.hasHttpOnlyFlag(cookieConfig)
|
|
601
|
+
) {
|
|
602
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
603
|
+
violations.push({
|
|
604
|
+
rule: this.ruleId,
|
|
605
|
+
source: filePath,
|
|
606
|
+
category: this.category,
|
|
607
|
+
line: lineNumber,
|
|
608
|
+
column: 1,
|
|
609
|
+
message: `Insecure session cookie: Nuxt useCookie "${cookieName}" missing httpOnly attribute`,
|
|
610
|
+
severity: "error",
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Handle other framework patterns
|
|
617
|
+
if (this.isSessionCookie(cookieName, matchText)) {
|
|
618
|
+
if (!this.hasHttpOnlyFlag(cookieConfig)) {
|
|
619
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
620
|
+
const framework = this.detectFramework(matchText);
|
|
621
|
+
|
|
622
|
+
violations.push({
|
|
623
|
+
rule: this.ruleId,
|
|
624
|
+
source: filePath,
|
|
625
|
+
category: this.category,
|
|
626
|
+
line: lineNumber,
|
|
627
|
+
column: 1,
|
|
628
|
+
message: `Insecure session cookie: ${framework} session cookie "${cookieName}" missing HttpOnly attribute`,
|
|
629
|
+
severity: "error",
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Check NextAuth configuration for httpOnly
|
|
637
|
+
*/
|
|
638
|
+
hasHttpOnlyInNextAuthConfig(config) {
|
|
639
|
+
// NextAuth typically has cookies.sessionToken.httpOnly configuration
|
|
640
|
+
return (
|
|
641
|
+
config.includes("httpOnly: true") ||
|
|
642
|
+
(config.includes("sessionToken") && config.includes("httpOnly")) ||
|
|
643
|
+
(config.includes("cookies") && config.includes("httpOnly"))
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Detect framework from match text
|
|
649
|
+
*/
|
|
650
|
+
detectFramework(matchText) {
|
|
651
|
+
if (matchText.includes("@Res()") || matchText.includes("NestJS")) {
|
|
652
|
+
return "NestJS";
|
|
653
|
+
} else if (
|
|
654
|
+
matchText.includes("NextResponse") ||
|
|
655
|
+
matchText.includes("NextAuth")
|
|
656
|
+
) {
|
|
657
|
+
return "Next.js";
|
|
658
|
+
} else if (
|
|
659
|
+
matchText.includes("useCookie") ||
|
|
660
|
+
matchText.includes("$cookies") ||
|
|
661
|
+
matchText.includes("nuxt")
|
|
662
|
+
) {
|
|
663
|
+
return "Nuxt.js";
|
|
664
|
+
}
|
|
665
|
+
return "Framework";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Clean up resources
|
|
670
|
+
*/
|
|
671
|
+
cleanup() {
|
|
672
|
+
// No resources to clean up for regex analyzer
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Check if configuration has httpOnly set to true
|
|
677
|
+
*/
|
|
678
|
+
hasHttpOnlyTrue(configText) {
|
|
679
|
+
// Remove comments to avoid false positives
|
|
680
|
+
const codeOnly = configText
|
|
681
|
+
.replace(/\/\/.*$/gm, "")
|
|
682
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
683
|
+
|
|
684
|
+
const httpOnlyPatterns = [
|
|
685
|
+
/httpOnly\s*:\s*true/i,
|
|
686
|
+
/httpOnly\s*=\s*true/i,
|
|
687
|
+
/['"]httpOnly['"]\s*:\s*true/i,
|
|
688
|
+
/HttpOnly/i, // For Set-Cookie header format
|
|
689
|
+
];
|
|
690
|
+
|
|
691
|
+
// Check for explicitly disabled httpOnly (should be treated as violation)
|
|
692
|
+
const httpOnlyDisabledPatterns = [
|
|
693
|
+
/httpOnly\s*:\s*false/i,
|
|
694
|
+
/httpOnly\s*=\s*false/i,
|
|
695
|
+
/['"]httpOnly['"]\s*:\s*false/i,
|
|
696
|
+
];
|
|
697
|
+
|
|
698
|
+
const hasHttpOnlyFalse = httpOnlyDisabledPatterns.some((pattern) =>
|
|
699
|
+
pattern.test(codeOnly)
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
const hasHttpOnly = httpOnlyPatterns.some((pattern) =>
|
|
703
|
+
pattern.test(codeOnly)
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
// If httpOnly is explicitly set to false, it's a violation
|
|
707
|
+
if (hasHttpOnlyFalse) {
|
|
708
|
+
return false; // Violation: explicitly disabled
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return hasHttpOnly;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
module.exports = S032RegexBasedAnalyzer;
|