@sun-asterisk/sunlint 1.3.26 → 1.3.28
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/config/rules/enhanced-rules-registry.json +101 -17
- package/config/rules/rules-registry-generated.json +22 -22
- package/origin-rules/security-en.md +351 -338
- package/package.json +1 -1
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
- package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
- package/rules/security/S003_open_redirect_protection/README.md +371 -0
- package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
- package/rules/security/S003_open_redirect_protection/config.json +58 -0
- package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
- package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
- package/rules/security/S004_sensitive_data_logging/config.json +62 -0
- package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
- package/rules/security/S005_no_origin_auth/config.json +28 -67
- package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
- package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
- package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
- package/rules/security/S012_hardcoded_secrets/config.json +75 -0
- package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
- package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
- package/rules/security/S019_smtp_injection_protection/config.json +35 -0
- package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
- package/rules/security/S022_escape_output_context/README.md +254 -0
- package/rules/security/S022_escape_output_context/analyzer.js +510 -0
- package/rules/security/S022_escape_output_context/config.json +229 -0
- package/rules/security/S023_no_json_injection/analyzer.js +15 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
- package/rules/security/S023_no_json_injection/config.json +133 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
- package/rules/security/S029_csrf_protection/config.json +127 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
- package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
- package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
- package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
- package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
- package/rules/security/S040_session_fixation_protection/config.json +20 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
- package/docs/COMMAND-EXAMPLES.md +0 -390
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
- package/docs/FOLDER_STRUCTURE.md +0 -59
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
|
@@ -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
|
+
}
|