@sun-asterisk/sunlint 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +5 -3
- package/config/rules/enhanced-rules-registry.json +144 -33
- package/core/analysis-orchestrator.js +167 -42
- package/core/auto-performance-manager.js +243 -0
- package/core/cli-action-handler.js +9 -1
- 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/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,724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S035 Regex-based Analyzer - Set Path attribute for Session Cookies
|
|
3
|
+
* Fallback analyzer for pattern-based detection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
|
|
8
|
+
class S035RegexBasedAnalyzer {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.ruleId = "S035";
|
|
11
|
+
this.ruleName = "Set Path attribute for Session Cookies";
|
|
12
|
+
this.description =
|
|
13
|
+
"Set Path attribute for Session Cookies to limit access scope";
|
|
14
|
+
|
|
15
|
+
// Regex patterns for detection
|
|
16
|
+
this.patterns = {
|
|
17
|
+
// Express.js patterns
|
|
18
|
+
cookieCall: /res\.cookie\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
19
|
+
setCookieHeader:
|
|
20
|
+
/res\.setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*['"]([^'"=]+)=/gi,
|
|
21
|
+
setCookieTemplate:
|
|
22
|
+
/res\.setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*`([^`=]+)=/gi,
|
|
23
|
+
setCookieArray:
|
|
24
|
+
/res\.setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*\[([^\]]+)\]/gi,
|
|
25
|
+
sessionMiddleware:
|
|
26
|
+
/session\s*\(\s*\{[^}]*name\s*:\s*['"`]([^'"`]+)['"`]/g,
|
|
27
|
+
|
|
28
|
+
// NestJS patterns
|
|
29
|
+
nestjsResCookie:
|
|
30
|
+
/@Res\(\)\s*\w+[^}]*\.cookie\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
31
|
+
nestjsCookieDecorator: /@Cookies\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
32
|
+
nestjsResponseCookie: /response\.cookie\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
33
|
+
|
|
34
|
+
// Next.js patterns
|
|
35
|
+
nextjsResponseCookiesSet:
|
|
36
|
+
/response\.cookies\.set\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
37
|
+
nextjsCookiesSet: /cookies\(\)\.set\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
38
|
+
nextjsSetCookie:
|
|
39
|
+
/NextResponse\.next\(\)\.cookies\.set\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
40
|
+
|
|
41
|
+
// NextAuth.js patterns
|
|
42
|
+
nextAuthSessionToken:
|
|
43
|
+
/sessionToken\s*:\s*\{[^}]*name\s*:\s*['"`]([^'"`]+)['"`]/g,
|
|
44
|
+
nextAuthCsrfToken:
|
|
45
|
+
/csrfToken\s*:\s*\{[^}]*name\s*:\s*['"`]([^'"`]+)['"`]/g,
|
|
46
|
+
nextAuthCookies: /cookies\s*:\s*\{[^}]*sessionToken\s*:/g,
|
|
47
|
+
|
|
48
|
+
// Session cookie names (expanded for frameworks)
|
|
49
|
+
sessionCookieNames:
|
|
50
|
+
/^(session|sessionid|session_id|sid|connect\.sid|auth|auth_token|authentication|jwt|token|csrf|csrf_token|xsrf|login|user|userid|user_id|sessionToken|csrfToken|next-auth\.session-token|next-auth\.csrf-token)$/i,
|
|
51
|
+
|
|
52
|
+
// Path attribute patterns
|
|
53
|
+
pathAttribute: /path\s*:\s*['"`]([^'"`]*)['"`]/gi,
|
|
54
|
+
pathInSetCookie: /Path=([^;\\s]*)/gi,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
this.violations = [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async initialize(semanticEngine) {
|
|
61
|
+
this.semanticEngine = semanticEngine;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async analyze(filePath) {
|
|
65
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
66
|
+
console.log(`🔍 [S035] Regex analysis starting for: ${filePath}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.violations = [];
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
73
|
+
const lines = content.split("\n");
|
|
74
|
+
|
|
75
|
+
// Analyze patterns
|
|
76
|
+
this.checkCookieCalls(content, lines);
|
|
77
|
+
this.checkSetCookieHeaders(content, lines);
|
|
78
|
+
this.checkSessionMiddleware(content, lines);
|
|
79
|
+
this.checkNestJSPatterns(content, lines);
|
|
80
|
+
this.checkNextJSPatterns(content, lines);
|
|
81
|
+
this.analyzeNextAuthConfig(content, lines);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn(`⚠ [S035] Regex analysis error:`, error.message);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
87
|
+
console.log(
|
|
88
|
+
`🔍 [S035] Regex analysis completed: ${this.violations.length} violations`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this.violations;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
checkCookieCalls(content, lines) {
|
|
96
|
+
let match;
|
|
97
|
+
this.patterns.cookieCall.lastIndex = 0;
|
|
98
|
+
|
|
99
|
+
while ((match = this.patterns.cookieCall.exec(content)) !== null) {
|
|
100
|
+
const cookieName = match[1];
|
|
101
|
+
|
|
102
|
+
if (this.isSessionCookie(cookieName)) {
|
|
103
|
+
// Check if this cookie call has path attribute
|
|
104
|
+
const cookieCallStart = match.index;
|
|
105
|
+
const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
|
|
106
|
+
|
|
107
|
+
if (cookieCallEnd > cookieCallStart) {
|
|
108
|
+
const cookieConfig = content.substring(
|
|
109
|
+
cookieCallStart,
|
|
110
|
+
cookieCallEnd
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (!this.hasPathAttribute(cookieConfig)) {
|
|
114
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
115
|
+
|
|
116
|
+
this.addViolation(
|
|
117
|
+
lineInfo.line,
|
|
118
|
+
lineInfo.column,
|
|
119
|
+
`Insecure session cookie: Session cookie "${cookieName}" (Express.js) missing Path attribute`
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
// Check if path is too broad (root path)
|
|
123
|
+
const pathMatch = cookieConfig.match(this.patterns.pathAttribute);
|
|
124
|
+
if (pathMatch && pathMatch[1] === "/") {
|
|
125
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
126
|
+
|
|
127
|
+
this.addViolation(
|
|
128
|
+
lineInfo.line,
|
|
129
|
+
lineInfo.column,
|
|
130
|
+
`Insecure session cookie: Session cookie "${cookieName}" (Express.js) uses root path "/", consider using a more specific path`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
checkSetCookieHeaders(content, lines) {
|
|
140
|
+
// Check direct Set-Cookie headers
|
|
141
|
+
let match;
|
|
142
|
+
this.patterns.setCookieHeader.lastIndex = 0;
|
|
143
|
+
|
|
144
|
+
while ((match = this.patterns.setCookieHeader.exec(content)) !== null) {
|
|
145
|
+
const cookieName = match[1];
|
|
146
|
+
|
|
147
|
+
if (this.isSessionCookie(cookieName)) {
|
|
148
|
+
// Get the full Set-Cookie value
|
|
149
|
+
const headerStart = content.indexOf('"', match.index);
|
|
150
|
+
const headerEnd = content.indexOf('"', headerStart + 1);
|
|
151
|
+
|
|
152
|
+
if (headerEnd > headerStart) {
|
|
153
|
+
const headerValue = content.substring(headerStart + 1, headerEnd);
|
|
154
|
+
|
|
155
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
156
|
+
console.log(
|
|
157
|
+
`🔍 [S035] Debug - Cookie: ${cookieName}, Header: ${headerValue}`
|
|
158
|
+
);
|
|
159
|
+
console.log(
|
|
160
|
+
`🔍 [S035] Debug - hasPath: ${this.hasPathInSetCookie(
|
|
161
|
+
headerValue
|
|
162
|
+
)}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!this.hasPathInSetCookie(headerValue)) {
|
|
167
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
168
|
+
|
|
169
|
+
this.addViolation(
|
|
170
|
+
lineInfo.line,
|
|
171
|
+
lineInfo.column,
|
|
172
|
+
`Insecure session cookie: Session cookie "${cookieName}" (Express.js) in Set-Cookie header missing Path attribute`
|
|
173
|
+
);
|
|
174
|
+
} else {
|
|
175
|
+
// Check if path is root
|
|
176
|
+
const pathMatch = headerValue.match(this.patterns.pathInSetCookie);
|
|
177
|
+
if (pathMatch && pathMatch[1] === "/") {
|
|
178
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
179
|
+
|
|
180
|
+
this.addViolation(
|
|
181
|
+
lineInfo.line,
|
|
182
|
+
lineInfo.column,
|
|
183
|
+
`Insecure session cookie: Session cookie "${cookieName}" (Express.js) uses root path "/", consider using a more specific path`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check Set-Cookie headers with template literals
|
|
192
|
+
this.patterns.setCookieTemplate.lastIndex = 0;
|
|
193
|
+
|
|
194
|
+
while ((match = this.patterns.setCookieTemplate.exec(content)) !== null) {
|
|
195
|
+
const cookieName = match[1];
|
|
196
|
+
|
|
197
|
+
if (this.isSessionCookie(cookieName)) {
|
|
198
|
+
// Get the full template literal value
|
|
199
|
+
const templateStart = content.indexOf("`", match.index);
|
|
200
|
+
const templateEnd = content.indexOf("`", templateStart + 1);
|
|
201
|
+
|
|
202
|
+
if (templateEnd > templateStart) {
|
|
203
|
+
const templateValue = content.substring(
|
|
204
|
+
templateStart + 1,
|
|
205
|
+
templateEnd
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
209
|
+
console.log(
|
|
210
|
+
`🔍 [S035] Debug Template - Cookie: ${cookieName}, Template: ${templateValue}`
|
|
211
|
+
);
|
|
212
|
+
console.log(
|
|
213
|
+
`🔍 [S035] Debug Template - hasPath: ${this.hasPathInSetCookie(
|
|
214
|
+
templateValue
|
|
215
|
+
)}`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!this.hasPathInSetCookie(templateValue)) {
|
|
220
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
221
|
+
|
|
222
|
+
this.addViolation(
|
|
223
|
+
lineInfo.line,
|
|
224
|
+
lineInfo.column,
|
|
225
|
+
`Session cookie "${cookieName}" in Set-Cookie header should specify Path attribute`
|
|
226
|
+
);
|
|
227
|
+
} else {
|
|
228
|
+
// Check for root path usage
|
|
229
|
+
const pathMatch = templateValue.match(/Path=([^;\\s]*)/gi);
|
|
230
|
+
if (pathMatch) {
|
|
231
|
+
const pathValue = pathMatch[0].replace(/Path=/gi, "");
|
|
232
|
+
if (
|
|
233
|
+
pathValue === "/" ||
|
|
234
|
+
pathValue === '""' ||
|
|
235
|
+
pathValue === "''"
|
|
236
|
+
) {
|
|
237
|
+
const lineInfo = this.findLineNumber(
|
|
238
|
+
content,
|
|
239
|
+
match.index,
|
|
240
|
+
lines
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
this.addViolation(
|
|
244
|
+
lineInfo.line,
|
|
245
|
+
lineInfo.column,
|
|
246
|
+
`Session cookie "${cookieName}" uses root path "/", consider using a more specific path`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check Set-Cookie arrays
|
|
256
|
+
this.patterns.setCookieArray.lastIndex = 0;
|
|
257
|
+
|
|
258
|
+
while ((match = this.patterns.setCookieArray.exec(content)) !== null) {
|
|
259
|
+
const arrayContent = match[1];
|
|
260
|
+
const cookieMatches = arrayContent.match(/['"`]([^'"`=]+)=/g);
|
|
261
|
+
|
|
262
|
+
if (cookieMatches) {
|
|
263
|
+
cookieMatches.forEach((cookieMatch) => {
|
|
264
|
+
const cookieName = cookieMatch.replace(/['"`]/g, "").replace("=", "");
|
|
265
|
+
|
|
266
|
+
if (this.isSessionCookie(cookieName)) {
|
|
267
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
268
|
+
console.log(
|
|
269
|
+
`🔍 [S035] Debug Array - Cookie: ${cookieName}, ArrayContent: ${arrayContent}`
|
|
270
|
+
);
|
|
271
|
+
console.log(
|
|
272
|
+
`🔍 [S035] Debug Array - hasPath: ${this.hasPathInSetCookie(
|
|
273
|
+
arrayContent
|
|
274
|
+
)}`
|
|
275
|
+
);
|
|
276
|
+
console.log(
|
|
277
|
+
`🔍 [S035] Debug Array - !hasPath: ${!this.hasPathInSetCookie(
|
|
278
|
+
arrayContent
|
|
279
|
+
)}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!this.hasPathInSetCookie(arrayContent)) {
|
|
284
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
285
|
+
|
|
286
|
+
this.addViolation(
|
|
287
|
+
lineInfo.line,
|
|
288
|
+
lineInfo.column,
|
|
289
|
+
`Session cookie "${cookieName}" in Set-Cookie array should specify Path attribute`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
checkSessionMiddleware(content, lines) {
|
|
299
|
+
let match;
|
|
300
|
+
this.patterns.sessionMiddleware.lastIndex = 0;
|
|
301
|
+
|
|
302
|
+
while ((match = this.patterns.sessionMiddleware.exec(content)) !== null) {
|
|
303
|
+
const cookieName = match[1];
|
|
304
|
+
|
|
305
|
+
if (this.isSessionCookie(cookieName)) {
|
|
306
|
+
// Check if session middleware has cookie.path configuration
|
|
307
|
+
const sessionStart = match.index;
|
|
308
|
+
const sessionEnd = this.findMatchingBrace(content, sessionStart);
|
|
309
|
+
|
|
310
|
+
if (sessionEnd > sessionStart) {
|
|
311
|
+
const sessionConfig = content.substring(sessionStart, sessionEnd);
|
|
312
|
+
|
|
313
|
+
if (!this.hasPathInCookieConfig(sessionConfig)) {
|
|
314
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
315
|
+
|
|
316
|
+
this.addViolation(
|
|
317
|
+
lineInfo.line,
|
|
318
|
+
lineInfo.column,
|
|
319
|
+
`Insecure session cookie: Session middleware cookie "${cookieName}" (Express.js) missing Path attribute`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Check NestJS specific patterns
|
|
329
|
+
*/
|
|
330
|
+
checkNestJSPatterns(content, lines) {
|
|
331
|
+
// Check @Res() decorator response.cookie calls
|
|
332
|
+
let match;
|
|
333
|
+
this.patterns.nestjsResCookie.lastIndex = 0;
|
|
334
|
+
|
|
335
|
+
while ((match = this.patterns.nestjsResCookie.exec(content)) !== null) {
|
|
336
|
+
const cookieName = match[1];
|
|
337
|
+
|
|
338
|
+
if (this.isSessionCookie(cookieName)) {
|
|
339
|
+
const cookieCallStart = match.index;
|
|
340
|
+
const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
|
|
341
|
+
|
|
342
|
+
if (cookieCallEnd > cookieCallStart) {
|
|
343
|
+
const cookieConfig = content.substring(
|
|
344
|
+
cookieCallStart,
|
|
345
|
+
cookieCallEnd
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (!this.hasPathAttribute(cookieConfig)) {
|
|
349
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
350
|
+
|
|
351
|
+
this.addViolation(
|
|
352
|
+
lineInfo.line,
|
|
353
|
+
lineInfo.column,
|
|
354
|
+
`Insecure session cookie: Session cookie "${cookieName}" (NestJS @Res) missing Path attribute`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check response.cookie calls in NestJS
|
|
362
|
+
this.patterns.nestjsResponseCookie.lastIndex = 0;
|
|
363
|
+
|
|
364
|
+
while (
|
|
365
|
+
(match = this.patterns.nestjsResponseCookie.exec(content)) !== null
|
|
366
|
+
) {
|
|
367
|
+
const cookieName = match[1];
|
|
368
|
+
|
|
369
|
+
if (this.isSessionCookie(cookieName)) {
|
|
370
|
+
const cookieCallStart = match.index;
|
|
371
|
+
const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
|
|
372
|
+
|
|
373
|
+
if (cookieCallEnd > cookieCallStart) {
|
|
374
|
+
const cookieConfig = content.substring(
|
|
375
|
+
cookieCallStart,
|
|
376
|
+
cookieCallEnd
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
if (!this.hasPathAttribute(cookieConfig)) {
|
|
380
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
381
|
+
|
|
382
|
+
this.addViolation(
|
|
383
|
+
lineInfo.line,
|
|
384
|
+
lineInfo.column,
|
|
385
|
+
`Insecure session cookie: Session cookie "${cookieName}" (NestJS) missing Path attribute`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Check @Cookies decorator usage
|
|
393
|
+
this.patterns.nestjsCookieDecorator.lastIndex = 0;
|
|
394
|
+
|
|
395
|
+
while (
|
|
396
|
+
(match = this.patterns.nestjsCookieDecorator.exec(content)) !== null
|
|
397
|
+
) {
|
|
398
|
+
const cookieName = match[1];
|
|
399
|
+
|
|
400
|
+
if (this.isSessionCookie(cookieName)) {
|
|
401
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
402
|
+
|
|
403
|
+
this.addViolation(
|
|
404
|
+
lineInfo.line,
|
|
405
|
+
lineInfo.column,
|
|
406
|
+
`Insecure session cookie: Session cookie "${cookieName}" (NestJS @Cookies) should specify Path attribute`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Check Next.js specific patterns
|
|
414
|
+
*/
|
|
415
|
+
checkNextJSPatterns(content, lines) {
|
|
416
|
+
// Check response.cookies.set() calls
|
|
417
|
+
let match;
|
|
418
|
+
this.patterns.nextjsResponseCookiesSet.lastIndex = 0;
|
|
419
|
+
|
|
420
|
+
while (
|
|
421
|
+
(match = this.patterns.nextjsResponseCookiesSet.exec(content)) !== null
|
|
422
|
+
) {
|
|
423
|
+
const cookieName = match[1];
|
|
424
|
+
|
|
425
|
+
if (this.isSessionCookie(cookieName)) {
|
|
426
|
+
const cookieCallStart = match.index;
|
|
427
|
+
const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
|
|
428
|
+
|
|
429
|
+
if (cookieCallEnd > cookieCallStart) {
|
|
430
|
+
const cookieConfig = content.substring(
|
|
431
|
+
cookieCallStart,
|
|
432
|
+
cookieCallEnd
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
if (!this.hasPathAttribute(cookieConfig)) {
|
|
436
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
437
|
+
|
|
438
|
+
this.addViolation(
|
|
439
|
+
lineInfo.line,
|
|
440
|
+
lineInfo.column,
|
|
441
|
+
`Insecure session cookie: Session cookie "${cookieName}" (Next.js) missing Path attribute`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Check cookies().set() from next/headers
|
|
449
|
+
this.patterns.nextjsCookiesSet.lastIndex = 0;
|
|
450
|
+
|
|
451
|
+
while ((match = this.patterns.nextjsCookiesSet.exec(content)) !== null) {
|
|
452
|
+
const cookieName = match[1];
|
|
453
|
+
|
|
454
|
+
if (this.isSessionCookie(cookieName)) {
|
|
455
|
+
const cookieCallStart = match.index;
|
|
456
|
+
const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
|
|
457
|
+
|
|
458
|
+
if (cookieCallEnd > cookieCallStart) {
|
|
459
|
+
const cookieConfig = content.substring(
|
|
460
|
+
cookieCallStart,
|
|
461
|
+
cookieCallEnd
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
if (!this.hasPathAttribute(cookieConfig)) {
|
|
465
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
466
|
+
|
|
467
|
+
this.addViolation(
|
|
468
|
+
lineInfo.line,
|
|
469
|
+
lineInfo.column,
|
|
470
|
+
`Insecure session cookie: Session cookie "${cookieName}" (Next.js headers) missing Path attribute`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Check NextResponse.next().cookies.set() calls
|
|
478
|
+
this.patterns.nextjsSetCookie.lastIndex = 0;
|
|
479
|
+
|
|
480
|
+
while ((match = this.patterns.nextjsSetCookie.exec(content)) !== null) {
|
|
481
|
+
const cookieName = match[1];
|
|
482
|
+
|
|
483
|
+
if (this.isSessionCookie(cookieName)) {
|
|
484
|
+
const cookieCallStart = match.index;
|
|
485
|
+
const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
|
|
486
|
+
|
|
487
|
+
if (cookieCallEnd > cookieCallStart) {
|
|
488
|
+
const cookieConfig = content.substring(
|
|
489
|
+
cookieCallStart,
|
|
490
|
+
cookieCallEnd
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
if (!this.hasPathAttribute(cookieConfig)) {
|
|
494
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
495
|
+
|
|
496
|
+
this.addViolation(
|
|
497
|
+
lineInfo.line,
|
|
498
|
+
lineInfo.column,
|
|
499
|
+
`Insecure session cookie: Session cookie "${cookieName}" (Next.js Response) missing Path attribute`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Analyze NextAuth.js configuration for session and CSRF tokens
|
|
509
|
+
*/
|
|
510
|
+
analyzeNextAuthConfig(content, lines) {
|
|
511
|
+
// Check sessionToken configuration
|
|
512
|
+
let match;
|
|
513
|
+
this.patterns.nextAuthSessionToken.lastIndex = 0;
|
|
514
|
+
|
|
515
|
+
while (
|
|
516
|
+
(match = this.patterns.nextAuthSessionToken.exec(content)) !== null
|
|
517
|
+
) {
|
|
518
|
+
const cookieName = match[1];
|
|
519
|
+
|
|
520
|
+
if (this.isSessionCookie(cookieName)) {
|
|
521
|
+
const tokenConfigStart = match.index;
|
|
522
|
+
const tokenConfigEnd = this.findMatchingBrace(
|
|
523
|
+
content,
|
|
524
|
+
tokenConfigStart
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
if (tokenConfigEnd > tokenConfigStart) {
|
|
528
|
+
const tokenConfig = content.substring(
|
|
529
|
+
tokenConfigStart,
|
|
530
|
+
tokenConfigEnd
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
if (!this.hasPathInOptionsConfig(tokenConfig)) {
|
|
534
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
535
|
+
|
|
536
|
+
this.addViolation(
|
|
537
|
+
lineInfo.line,
|
|
538
|
+
lineInfo.column,
|
|
539
|
+
`Insecure session cookie: NextAuth sessionToken "${cookieName}" missing Path attribute in options`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Check csrfToken configuration
|
|
547
|
+
this.patterns.nextAuthCsrfToken.lastIndex = 0;
|
|
548
|
+
|
|
549
|
+
while ((match = this.patterns.nextAuthCsrfToken.exec(content)) !== null) {
|
|
550
|
+
const cookieName = match[1];
|
|
551
|
+
|
|
552
|
+
if (this.isSessionCookie(cookieName)) {
|
|
553
|
+
const tokenConfigStart = match.index;
|
|
554
|
+
const tokenConfigEnd = this.findMatchingBrace(
|
|
555
|
+
content,
|
|
556
|
+
tokenConfigStart
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
if (tokenConfigEnd > tokenConfigStart) {
|
|
560
|
+
const tokenConfig = content.substring(
|
|
561
|
+
tokenConfigStart,
|
|
562
|
+
tokenConfigEnd
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
if (!this.hasPathInOptionsConfig(tokenConfig)) {
|
|
566
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
567
|
+
|
|
568
|
+
this.addViolation(
|
|
569
|
+
lineInfo.line,
|
|
570
|
+
lineInfo.column,
|
|
571
|
+
`Insecure session cookie: NextAuth csrfToken "${cookieName}" missing Path attribute in options`
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Check general NextAuth cookies configuration
|
|
579
|
+
this.patterns.nextAuthCookies.lastIndex = 0;
|
|
580
|
+
|
|
581
|
+
while ((match = this.patterns.nextAuthCookies.exec(content)) !== null) {
|
|
582
|
+
const cookiesConfigStart = match.index;
|
|
583
|
+
const cookiesConfigEnd = this.findMatchingBrace(
|
|
584
|
+
content,
|
|
585
|
+
cookiesConfigStart
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
if (cookiesConfigEnd > cookiesConfigStart) {
|
|
589
|
+
const cookiesConfig = content.substring(
|
|
590
|
+
cookiesConfigStart,
|
|
591
|
+
cookiesConfigEnd
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
// Check if sessionToken is configured without path
|
|
595
|
+
if (
|
|
596
|
+
cookiesConfig.includes("sessionToken") &&
|
|
597
|
+
!this.hasPathInNextAuthConfig(cookiesConfig)
|
|
598
|
+
) {
|
|
599
|
+
const lineInfo = this.findLineNumber(content, match.index, lines);
|
|
600
|
+
|
|
601
|
+
this.addViolation(
|
|
602
|
+
lineInfo.line,
|
|
603
|
+
lineInfo.column,
|
|
604
|
+
`Insecure session cookie: NextAuth cookies configuration missing Path attribute for session tokens`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
hasPathAttribute(config) {
|
|
612
|
+
return this.patterns.pathAttribute.test(config);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
hasPathInSetCookie(headerValue) {
|
|
616
|
+
this.patterns.pathInSetCookie.lastIndex = 0; // Reset global regex
|
|
617
|
+
return this.patterns.pathInSetCookie.test(headerValue);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
hasPathInCookieConfig(config) {
|
|
621
|
+
// Check for cookie: { path: ... } pattern
|
|
622
|
+
return /cookie\s*:\s*\{[^}]*path\s*:/i.test(config);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Check if NextAuth options configuration has path attribute
|
|
627
|
+
*/
|
|
628
|
+
hasPathInOptionsConfig(config) {
|
|
629
|
+
// Check for options: { path: ... } pattern in NextAuth
|
|
630
|
+
return /options\s*:\s*\{[^}]*path\s*:/i.test(config);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Check if NextAuth cookies configuration has path attribute
|
|
635
|
+
*/
|
|
636
|
+
hasPathInNextAuthConfig(config) {
|
|
637
|
+
// Check for path attribute in NextAuth cookies configuration
|
|
638
|
+
return (
|
|
639
|
+
/path\s*:\s*['"`][^'"`]*['"`]/i.test(config) ||
|
|
640
|
+
/options\s*:\s*\{[^}]*path\s*:/i.test(config)
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
findMatchingBrace(content, startIndex) {
|
|
645
|
+
let braceCount = 0;
|
|
646
|
+
let inString = false;
|
|
647
|
+
let stringChar = "";
|
|
648
|
+
|
|
649
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
650
|
+
const char = content[i];
|
|
651
|
+
|
|
652
|
+
if (!inString) {
|
|
653
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
654
|
+
inString = true;
|
|
655
|
+
stringChar = char;
|
|
656
|
+
} else if (char === "{") {
|
|
657
|
+
braceCount++;
|
|
658
|
+
} else if (char === "}") {
|
|
659
|
+
braceCount--;
|
|
660
|
+
if (braceCount === 0) {
|
|
661
|
+
return i + 1;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
} else {
|
|
665
|
+
if (char === stringChar && content[i - 1] !== "\\") {
|
|
666
|
+
inString = false;
|
|
667
|
+
stringChar = "";
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return startIndex + 1000; // Fallback
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
isSessionCookie(cookieName) {
|
|
676
|
+
return this.patterns.sessionCookieNames.test(cookieName);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
findLineNumber(content, position, lines) {
|
|
680
|
+
let currentPos = 0;
|
|
681
|
+
|
|
682
|
+
for (let i = 0; i < lines.length; i++) {
|
|
683
|
+
const lineLength = lines[i].length + 1; // +1 for newline
|
|
684
|
+
|
|
685
|
+
if (currentPos + lineLength > position) {
|
|
686
|
+
return {
|
|
687
|
+
line: i + 1,
|
|
688
|
+
column: position - currentPos + 1,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
currentPos += lineLength;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return { line: lines.length, column: 1 };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
addViolation(line, column, message) {
|
|
699
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
700
|
+
console.log(
|
|
701
|
+
`🔍 [S035] Regex violation at line ${line}, column ${column}: ${message.substring(
|
|
702
|
+
0,
|
|
703
|
+
50
|
|
704
|
+
)}...`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
this.violations.push({
|
|
709
|
+
ruleId: this.ruleId,
|
|
710
|
+
ruleName: this.ruleName,
|
|
711
|
+
severity: "warning",
|
|
712
|
+
message: message,
|
|
713
|
+
line: line,
|
|
714
|
+
column: column,
|
|
715
|
+
source: "regex-based",
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
cleanup() {
|
|
720
|
+
this.violations = [];
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
module.exports = S035RegexBasedAnalyzer;
|