@sun-asterisk/sunlint 1.3.26 → 1.3.27

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.
Files changed (43) hide show
  1. package/config/rules/enhanced-rules-registry.json +99 -16
  2. package/package.json +1 -1
  3. package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
  4. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
  5. package/rules/security/S003_open_redirect_protection/README.md +371 -0
  6. package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
  7. package/rules/security/S003_open_redirect_protection/config.json +58 -0
  8. package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
  9. package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
  10. package/rules/security/S004_sensitive_data_logging/config.json +62 -0
  11. package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
  12. package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
  13. package/rules/security/S012_hardcoded_secrets/config.json +75 -0
  14. package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
  15. package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
  16. package/rules/security/S019_smtp_injection_protection/config.json +35 -0
  17. package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
  18. package/rules/security/S022_escape_output_context/README.md +254 -0
  19. package/rules/security/S022_escape_output_context/analyzer.js +510 -0
  20. package/rules/security/S022_escape_output_context/config.json +229 -0
  21. package/rules/security/S023_no_json_injection/analyzer.js +15 -0
  22. package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
  23. package/rules/security/S023_no_json_injection/config.json +133 -0
  24. package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
  25. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
  26. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
  27. package/rules/security/S029_csrf_protection/config.json +127 -0
  28. package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
  29. package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
  30. package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
  31. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
  32. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
  33. package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
  34. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
  35. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
  36. package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
  37. package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
  38. package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
  39. package/rules/security/S040_session_fixation_protection/config.json +20 -0
  40. package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
  41. package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
  42. package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
  43. package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
@@ -0,0 +1,371 @@
1
+ # S003 - Open Redirect Protection
2
+
3
+ ## Overview
4
+
5
+ Quy tắc này phát hiện lỗ hổng Open Redirect - nơi ứng dụng chuyển hướng người dùng đến URL được cung cấp từ đầu vào mà không xác thực. Kẻ tấn công có thể lợi dụng để chuyển hướng nạn nhân đến trang độc hại (phishing, malware) thông qua link có vẻ hợp lệ.
6
+
7
+ ## OWASP Classification
8
+
9
+ - **Category**: A03:2021 - Injection
10
+ - **CWE**: CWE-601 - URL Redirection to Untrusted Site ('Open Redirect')
11
+ - **Severity**: Warning
12
+ - **Impact**: Medium (Phishing attacks, credential theft, malware distribution)
13
+
14
+ ## Vấn đề
15
+
16
+ Khi ứng dụng chuyển hướng người dùng đến URL từ đầu vào mà không xác thực:
17
+
18
+ 1. **Phishing attacks**: Kẻ tấn công có thể tạo link hợp lệ dẫn đến trang giả mạo
19
+ 2. **Credential theft**: Người dùng tin tưởng domain gốc và nhập thông tin nhạy cảm
20
+ 3. **Malware distribution**: Chuyển hướng đến trang chứa malware
21
+ 4. **Bypass security controls**: Vượt qua whitelist/blacklist dựa trên domain
22
+
23
+ ## Các trường hợp vi phạm
24
+
25
+ ### 1. Redirect trực tiếp từ query parameter
26
+
27
+ ```javascript
28
+ // ❌ Vi phạm - Redirect trực tiếp không kiểm tra
29
+ app.get('/redirect', (req, res) => {
30
+ const url = req.query.url;
31
+ res.redirect(url); // Nguy hiểm!
32
+ });
33
+
34
+ // ❌ Vi phạm - Express redirect
35
+ router.get('/goto', (req, res) => {
36
+ res.redirect(req.query.redirect_url);
37
+ });
38
+
39
+ // ❌ Vi phạm - Location header
40
+ app.get('/forward', (req, res) => {
41
+ res.setHeader('Location', req.query.next);
42
+ res.status(302).send();
43
+ });
44
+ ```
45
+
46
+ ### 2. Client-side redirect không validate
47
+
48
+ ```javascript
49
+ // ❌ Vi phạm - window.location redirect
50
+ const redirectUrl = new URLSearchParams(window.location.search).get('redirect');
51
+ window.location = redirectUrl;
52
+
53
+ // ❌ Vi phạm - window.location.href
54
+ const next = getQueryParam('next');
55
+ window.location.href = next;
56
+
57
+ // ❌ Vi phạm - window.location.replace
58
+ window.location.replace(req.params.url);
59
+ ```
60
+
61
+ ### 3. NestJS redirects
62
+
63
+ ```typescript
64
+ // ❌ Vi phạm - NestJS @Redirect decorator
65
+ @Controller('redirect')
66
+ export class RedirectController {
67
+ @Get()
68
+ @Redirect()
69
+ redirect(@Query('url') url: string) {
70
+ return { url }; // Nguy hiểm!
71
+ }
72
+ }
73
+
74
+ // ❌ Vi phạm - NestJS res.redirect
75
+ @Get()
76
+ goto(@Query('target') target: string, @Res() res: Response) {
77
+ res.redirect(target);
78
+ }
79
+ ```
80
+
81
+ ### 4. Next.js redirects (App Router)
82
+
83
+ ```typescript
84
+ // ❌ Vi phạm - Next.js redirect
85
+ import { redirect } from 'next/navigation';
86
+
87
+ export default async function Page({ searchParams }) {
88
+ redirect(searchParams.url); // Nguy hiểm!
89
+ }
90
+
91
+ // ❌ Vi phạm - Next.js permanentRedirect
92
+ import { permanentRedirect } from 'next/navigation';
93
+
94
+ export default function RedirectPage({ searchParams }) {
95
+ permanentRedirect(searchParams.target);
96
+ }
97
+
98
+ // ❌ Vi phạm - Next.js router.push
99
+ 'use client';
100
+ export default function ClientRedirect() {
101
+ const router = useRouter();
102
+ const searchParams = useSearchParams();
103
+
104
+ const url = searchParams.get('url');
105
+ router.push(url); // Nguy hiểm!
106
+ }
107
+ ```
108
+
109
+ ### 5. Nuxt.js redirects
110
+
111
+ ```typescript
112
+ // ❌ Vi phạm - Nuxt.js navigateTo
113
+ export default defineComponent({
114
+ setup() {
115
+ const route = useRoute();
116
+ const url = route.query.url;
117
+ navigateTo(url); // Nguy hiểm!
118
+ }
119
+ });
120
+
121
+ // ❌ Vi phạm - Nuxt.js sendRedirect
122
+ export default defineEventHandler((event) => {
123
+ const { url } = getQuery(event);
124
+ return sendRedirect(event, url);
125
+ });
126
+ ```
127
+
128
+ ### 6. Spring Boot/Java redirects
129
+
130
+ ```java
131
+ // ❌ Vi phạm - Spring RedirectView
132
+ @GetMapping("/redirect")
133
+ public RedirectView redirect(@RequestParam String url) {
134
+ return new RedirectView(url);
135
+ }
136
+
137
+ // ❌ Vi phạm - sendRedirect
138
+ @GetMapping("/forward")
139
+ public void forward(@RequestParam String target, HttpServletResponse response) {
140
+ response.sendRedirect(target);
141
+ }
142
+ ```
143
+
144
+ ## Giải pháp an toàn
145
+
146
+ ### 1. Sử dụng Allow List (Whitelist)
147
+
148
+ ```javascript
149
+ // ✅ An toàn - Allow list
150
+ const ALLOWED_DOMAINS = [
151
+ 'https://example.com',
152
+ 'https://app.example.com'
153
+ ];
154
+
155
+ app.get('/redirect', (req, res) => {
156
+ const url = req.query.url;
157
+ if (!ALLOWED_DOMAINS.includes(url)) {
158
+ return res.status(400).send('Invalid redirect URL');
159
+ }
160
+ res.redirect(url);
161
+ });
162
+
163
+ // ✅ An toàn - Domain validation
164
+ function isAllowedUrl(urlString) {
165
+ try {
166
+ const url = new URL(urlString);
167
+ const allowedHosts = ['example.com', 'app.example.com'];
168
+ return allowedHosts.includes(url.hostname);
169
+ } catch (e) {
170
+ return false;
171
+ }
172
+ }
173
+ ```
174
+
175
+ ### 2. Relative URL only
176
+
177
+ ```javascript
178
+ // ✅ An toàn - Chỉ cho phép relative URLs
179
+ function isRelativeUrl(url) {
180
+ return url && url.startsWith('/') && !url.startsWith('//');
181
+ }
182
+
183
+ app.get('/redirect', (req, res) => {
184
+ const path = req.query.next;
185
+ if (!isRelativeUrl(path)) {
186
+ return res.status(400).send('Only relative URLs allowed');
187
+ }
188
+ res.redirect(path);
189
+ });
190
+ ```
191
+
192
+ ### 3. Indirect redirect mapping
193
+
194
+ ```javascript
195
+ // ✅ An toàn - Mapping key
196
+ const REDIRECT_MAP = {
197
+ 'home': '/',
198
+ 'dashboard': '/dashboard',
199
+ 'profile': '/user/profile'
200
+ };
201
+
202
+ app.get('/redirect', (req, res) => {
203
+ const key = req.query.destination;
204
+ const url = REDIRECT_MAP[key];
205
+ if (!url) {
206
+ return res.status(400).send('Invalid destination');
207
+ }
208
+ res.redirect(url);
209
+ });
210
+ ```
211
+
212
+ ### 4. Framework-specific solutions
213
+
214
+ #### NestJS
215
+ ```typescript
216
+ // ✅ An toàn - NestJS với DTO validation
217
+ import { IsIn } from 'class-validator';
218
+
219
+ class RedirectDto {
220
+ @IsIn(['home', 'dashboard', 'profile'])
221
+ destination: string;
222
+ }
223
+
224
+ @Controller('redirect')
225
+ export class SafeController {
226
+ @Get()
227
+ redirect(@Query() query: RedirectDto) {
228
+ const urlMap = {
229
+ home: '/',
230
+ dashboard: '/dashboard',
231
+ profile: '/profile'
232
+ };
233
+ return { url: urlMap[query.destination] }; // Safe
234
+ }
235
+ }
236
+
237
+ // ✅ An toàn - NestJS với custom validation
238
+ @Controller('goto')
239
+ export class ValidatedController {
240
+ private readonly allowedUrls = ['https://example.com'];
241
+
242
+ @Get()
243
+ goto(@Query('url') url: string, @Res() res: Response) {
244
+ if (this.allowedUrls.includes(url)) {
245
+ res.redirect(url); // Safe
246
+ } else {
247
+ throw new BadRequestException('Invalid URL');
248
+ }
249
+ }
250
+ }
251
+ ```
252
+
253
+ #### Next.js (App Router)
254
+ ```typescript
255
+ // ✅ An toàn - Next.js với allowlist
256
+ const ALLOWED_PATHS = ['/home', '/dashboard', '/profile'];
257
+
258
+ export default function SafeRedirect({ searchParams }) {
259
+ const path = searchParams.path;
260
+
261
+ if (ALLOWED_PATHS.includes(path)) {
262
+ redirect(path); // Safe
263
+ } else {
264
+ redirect('/'); // Safe default
265
+ }
266
+ }
267
+
268
+ // ✅ An toàn - Next.js với mapping
269
+ const REDIRECT_MAP = {
270
+ 'success': '/success',
271
+ 'error': '/error'
272
+ };
273
+
274
+ export default function MappedRedirect({ searchParams }) {
275
+ const key = searchParams.destination;
276
+ redirect(REDIRECT_MAP[key] || '/'); // Safe
277
+ }
278
+
279
+ // ✅ An toàn - Next.js router với validation
280
+ 'use client';
281
+ export default function SafeClientRedirect() {
282
+ const router = useRouter();
283
+ const searchParams = useSearchParams();
284
+
285
+ const handleRedirect = () => {
286
+ const path = searchParams.get('path');
287
+ const safePaths = ['/home', '/about'];
288
+
289
+ if (safePaths.includes(path)) {
290
+ router.push(path); // Safe
291
+ }
292
+ };
293
+ }
294
+ ```
295
+
296
+ #### Nuxt.js
297
+ ```typescript
298
+ // ✅ An toàn - Nuxt.js với validation
299
+ const ALLOWED_PATHS = ['/home', '/dashboard'];
300
+
301
+ export default defineEventHandler((event) => {
302
+ const { path } = getQuery(event);
303
+
304
+ if (ALLOWED_PATHS.includes(path)) {
305
+ return navigateTo(path); // Safe
306
+ }
307
+
308
+ return navigateTo('/'); // Safe default
309
+ });
310
+
311
+ // ✅ An toàn - Nuxt.js với mapping
312
+ const ROUTE_MAP = {
313
+ 'profile': '/user/profile',
314
+ 'settings': '/settings'
315
+ };
316
+
317
+ export default defineComponent({
318
+ setup() {
319
+ const route = useRoute();
320
+
321
+ const redirect = () => {
322
+ const key = route.query.destination;
323
+ const url = ROUTE_MAP[key] || '/';
324
+ navigateTo(url); // Safe
325
+ };
326
+ }
327
+ });
328
+
329
+ // ✅ An toàn - Nuxt.js với domain validation
330
+ export default defineEventHandler((event) => {
331
+ const { url } = getQuery(event);
332
+ const allowedDomains = ['example.com'];
333
+
334
+ try {
335
+ const parsed = new URL(url);
336
+ if (allowedDomains.includes(parsed.hostname)) {
337
+ return sendRedirect(event, url); // Safe
338
+ }
339
+ } catch {
340
+ throw createError({ statusCode: 400 });
341
+ }
342
+ });
343
+ ```
344
+
345
+ ## Phương pháp phát hiện
346
+
347
+ Rule này sử dụng symbol-based analysis để phát hiện:
348
+
349
+ 1. **Redirect functions**:
350
+ - Express: `res.redirect()`
351
+ - NestJS: `@Redirect()`, `res.redirect()`
352
+ - Next.js: `redirect()`, `permanentRedirect()`, `router.push()`
353
+ - Nuxt.js: `navigateTo()`, `sendRedirect()`
354
+ - Spring: `response.sendRedirect()`
355
+ - Generic: `window.location`, `setHeader('Location')`
356
+
357
+ 2. **User input sources**:
358
+ - `req.query`, `req.params`, `req.body`
359
+ - `@Query()`, `@Param()`, `@Body()` (NestJS)
360
+ - `searchParams`, `useSearchParams()` (Next.js)
361
+ - `useRoute()`, `getQuery()`, `event.query` (Nuxt.js)
362
+ - `URLSearchParams.get()`
363
+
364
+ 3. **Dataflow analysis**: Track từ user input đến redirect function
365
+ 4. **Validation check**: Kiểm tra có whitelist/validation hay không
366
+
367
+ ## Tài liệu tham khảo
368
+
369
+ - [OWASP A03:2021 - Injection](https://owasp.org/Top10/A03_2021-Injection/)
370
+ - [CWE-601: URL Redirection to Untrusted Site](https://cwe.mitre.org/data/definitions/601.html)
371
+ - [OWASP Unvalidated Redirects Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html)
@@ -0,0 +1,135 @@
1
+ /**
2
+ * S003 - Open Redirect Protection
3
+ *
4
+ * Main analyzer using symbol-based analysis to detect unvalidated URL redirects
5
+ * from user input.
6
+ *
7
+ * Based on:
8
+ * - OWASP A03:2021 - Injection
9
+ * - CWE-601: URL Redirection to Untrusted Site ('Open Redirect')
10
+ */
11
+
12
+ // Command: node cli.js --rule=S003 --input=examples/rule-test-fixtures/rules/S003_open_redirect_protection --engine=heuristic
13
+
14
+ const S003SymbolBasedAnalyzer = require("./symbol-based-analyzer");
15
+
16
+ class S003Analyzer {
17
+ constructor(options = {}) {
18
+ this.ruleId = "S003";
19
+ this.semanticEngine = options.semanticEngine || null;
20
+ this.verbose = options.verbose || false;
21
+
22
+ try {
23
+ this.symbolAnalyzer = new S003SymbolBasedAnalyzer(this.semanticEngine);
24
+ } catch (e) {
25
+ console.warn(`⚠ [S003] Failed to create symbol analyzer: ${e.message}`);
26
+ }
27
+ }
28
+
29
+ async initialize(semanticEngine) {
30
+ this.semanticEngine = semanticEngine;
31
+ if (this.symbolAnalyzer && this.symbolAnalyzer.initialize) {
32
+ await this.symbolAnalyzer.initialize(semanticEngine);
33
+ }
34
+ }
35
+
36
+ analyzeSingle(filePath, options = {}) {
37
+ return this.analyze([filePath], "typescript", options);
38
+ }
39
+
40
+ async analyze(files, language, options = {}) {
41
+ const violations = [];
42
+ for (const filePath of files) {
43
+ try {
44
+ const vs = await this.analyzeFile(filePath, options);
45
+ violations.push(...vs);
46
+ } catch (e) {
47
+ console.warn(`⚠ [S003] Analysis error for ${filePath}: ${e.message}`);
48
+ }
49
+ }
50
+ return violations;
51
+ }
52
+
53
+ async analyzeFile(filePath, options = {}) {
54
+ const violationMap = new Map();
55
+
56
+ if (!this.symbolAnalyzer) {
57
+ return [];
58
+ }
59
+
60
+ // Skip test files, build directories, and node_modules
61
+ if (this.shouldSkipFile(filePath)) {
62
+ return [];
63
+ }
64
+
65
+ try {
66
+ let sourceFile = null;
67
+ if (this.semanticEngine?.project) {
68
+ sourceFile = this.semanticEngine.project.getSourceFile(filePath);
69
+ }
70
+
71
+ if (!sourceFile) {
72
+ // Create temporary ts-morph source file
73
+ const fs = require("fs");
74
+ const path = require("path");
75
+ const { Project } = require("ts-morph");
76
+ if (!fs.existsSync(filePath)) {
77
+ throw new Error(`File not found: ${filePath}`);
78
+ }
79
+ const content = fs.readFileSync(filePath, "utf8");
80
+ const tmp = new Project({
81
+ useInMemoryFileSystem: true,
82
+ compilerOptions: { allowJs: true },
83
+ });
84
+ sourceFile = tmp.createSourceFile(path.basename(filePath), content);
85
+ }
86
+
87
+ if (sourceFile) {
88
+ const symbolViolations = await this.symbolAnalyzer.analyze(
89
+ sourceFile,
90
+ filePath
91
+ );
92
+ symbolViolations.forEach((v) => {
93
+ const key = `${v.line}:${v.column}:${v.message}`;
94
+ if (!violationMap.has(key)) violationMap.set(key, v);
95
+ });
96
+ }
97
+ } catch (e) {
98
+ console.warn(`⚠ [S003] Symbol analysis failed: ${e.message}`);
99
+ }
100
+
101
+ return Array.from(violationMap.values()).map((v) => ({
102
+ ...v,
103
+ filePath,
104
+ file: filePath,
105
+ }));
106
+ }
107
+
108
+ shouldSkipFile(filePath) {
109
+ const skipPatterns = [
110
+ "test/",
111
+ "tests/",
112
+ "__tests__/",
113
+ ".test.",
114
+ ".spec.",
115
+ "node_modules/",
116
+ "build/",
117
+ "dist/",
118
+ ".next/",
119
+ "coverage/",
120
+ "vendor/",
121
+ "mocks/",
122
+ ".mock.",
123
+ ];
124
+
125
+ return skipPatterns.some((pattern) => filePath.includes(pattern));
126
+ }
127
+
128
+ cleanup() {
129
+ if (this.symbolAnalyzer?.cleanup) {
130
+ this.symbolAnalyzer.cleanup();
131
+ }
132
+ }
133
+ }
134
+
135
+ module.exports = S003Analyzer;
@@ -0,0 +1,58 @@
1
+ {
2
+ "ruleId": "S003",
3
+ "name": "Open Redirect Protection",
4
+ "description": "URL redirects must validate against an allow list to prevent open redirect vulnerabilities",
5
+ "category": "security",
6
+ "severity": "warning",
7
+ "languages": ["All languages"],
8
+ "tags": [
9
+ "security",
10
+ "owasp",
11
+ "injection",
12
+ "open-redirect",
13
+ "phishing",
14
+ "url-validation"
15
+ ],
16
+ "enabled": true,
17
+ "fixable": false,
18
+ "engine": "heuristic",
19
+ "metadata": {
20
+ "owaspCategory": "A03:2021 - Injection",
21
+ "cweId": "CWE-601",
22
+ "description": "Applications that redirect users to URLs from untrusted input without validation are vulnerable to phishing attacks. Attackers can create legitimate-looking links that redirect victims to malicious sites.",
23
+ "impact": "Medium - Phishing attacks, credential theft, malware distribution",
24
+ "likelihood": "High",
25
+ "remediation": "Use allow list (whitelist) to validate redirect URLs, or only allow relative URLs within the same domain"
26
+ },
27
+ "patterns": {
28
+ "vulnerable": [
29
+ "res.redirect(req.query.*) without validation",
30
+ "res.redirect(req.params.*) without validation",
31
+ "window.location = userInput without validation",
32
+ "response.sendRedirect(request.getParameter(*)) without validation",
33
+ "new RedirectView(userInput) without validation",
34
+ "res.setHeader('Location', req.query.*) without validation"
35
+ ],
36
+ "secure": [
37
+ "Validate against allow list (whitelist) of trusted domains",
38
+ "Only allow relative URLs (startsWith('/') && !startsWith('//'))",
39
+ "Use indirect mapping (key -> URL mapping)",
40
+ "Parse and validate URL with new URL() and check hostname",
41
+ "Use framework validation (e.g., @IsIn(['home', 'dashboard']))"
42
+ ]
43
+ },
44
+ "examples": {
45
+ "violations": [
46
+ "res.redirect(req.query.url);",
47
+ "window.location.href = params.get('redirect');",
48
+ "response.sendRedirect(request.getParameter('next'));",
49
+ "new RedirectView(request.getParameter('url'));"
50
+ ],
51
+ "fixes": [
52
+ "const url = req.query.url; if (ALLOWED_URLS.includes(url)) res.redirect(url);",
53
+ "if (isAllowedDomain(url)) window.location.href = url;",
54
+ "const destination = REDIRECT_MAP[req.query.key]; res.redirect(destination);",
55
+ "if (url.startsWith('/') && !url.startsWith('//')) res.redirect(url);"
56
+ ]
57
+ }
58
+ }