@sun-asterisk/sunlint 1.3.7 → 1.3.9
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 +63 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +247 -53
- package/core/file-targeting-service.js +98 -7
- package/package.json +1 -1
- package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
- package/rules/common/C065_one_behavior_per_test/config.json +95 -0
- package/rules/security/S020_no_eval_dynamic_code/README.md +136 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +263 -0
- package/rules/security/S020_no_eval_dynamic_code/config.json +54 -0
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +307 -0
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +280 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +3 -3
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +3 -4
- package/rules/security/S030_directory_browsing_protection/README.md +128 -0
- package/rules/security/S030_directory_browsing_protection/analyzer.js +264 -0
- package/rules/security/S030_directory_browsing_protection/config.json +63 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +483 -0
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +539 -0
- package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +8 -9
- package/rules/security/S037_cache_headers/README.md +128 -0
- package/rules/security/S037_cache_headers/analyzer.js +263 -0
- package/rules/security/S037_cache_headers/config.json +50 -0
- package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
- package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
- package/rules/security/S038_no_version_headers/README.md +234 -0
- package/rules/security/S038_no_version_headers/analyzer.js +262 -0
- package/rules/security/S038_no_version_headers/config.json +49 -0
- package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
- package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
- package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
- package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
- package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
- package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
- package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +443 -0
- package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
- package/rules/security/S049_short_validity_tokens/config.json +124 -0
- package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
- package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
- package/rules/security/S051_password_length_policy/analyzer.js +410 -0
- package/rules/security/S051_password_length_policy/config.json +83 -0
- package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
- package/rules/security/S052_weak_otp_entropy/config.json +57 -0
- package/rules/security/S054_no_default_accounts/README.md +129 -0
- package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
- package/rules/security/S054_no_default_accounts/config.json +101 -0
- package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
- package/rules/security/S056_log_injection_protection/config.json +148 -0
- package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
- package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +246 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ruleId": "S054",
|
|
3
|
+
"name": "Disallow Default/Built-in Accounts (admin/root/sa/...)",
|
|
4
|
+
"description": "Prevent use of default or shared accounts. Enforce per-user identities, initial password change, and disabling well-known built-ins.",
|
|
5
|
+
"category": "security",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"options": {
|
|
8
|
+
"blockedUsernames": [
|
|
9
|
+
"admin","root","sa","test","guest","operator","super","superuser","sys",
|
|
10
|
+
"postgres","mysql","mssql","oracle","elastic","kibana","grafana",
|
|
11
|
+
"administrator", "demo", "example", "default", "public", "anonymous",
|
|
12
|
+
"user", "password", "service", "support", "backup", "monitor"
|
|
13
|
+
],
|
|
14
|
+
"codeCreationPatterns": [
|
|
15
|
+
"create(User|Account)\\s*\\(",
|
|
16
|
+
"new\\s+User\\s*\\(",
|
|
17
|
+
"user(Name|name|_name)\\s*:",
|
|
18
|
+
"username\\s*=\\s*",
|
|
19
|
+
"setUser(Name|name)\\s*\\(",
|
|
20
|
+
"addUser\\s*\\(",
|
|
21
|
+
"registerUser\\s*\\(",
|
|
22
|
+
"createAccount\\s*\\("
|
|
23
|
+
],
|
|
24
|
+
"sqlInsertUserPatterns": [
|
|
25
|
+
"INSERT\\s+INTO\\s+\\w*user\\w*\\s*\\(",
|
|
26
|
+
"UPSERT\\s+INTO\\s+\\w*user\\w*\\s*\\(",
|
|
27
|
+
"CREATE\\s+USER\\s+",
|
|
28
|
+
"GRANT\\s+.+\\s+TO\\s+",
|
|
29
|
+
"REVOKE\\s+.+\\s+FROM\\s+"
|
|
30
|
+
],
|
|
31
|
+
"infraPatterns": {
|
|
32
|
+
"terraform": [
|
|
33
|
+
"username\\s*=\\s*\"(admin|root|sa|test|guest)\"",
|
|
34
|
+
"user\\s*=\\s*\"(admin|root|sa|test|guest)\"",
|
|
35
|
+
"admin_username\\s*=\\s*\"(admin|root|sa|test|guest)\""
|
|
36
|
+
],
|
|
37
|
+
"helmValues": [
|
|
38
|
+
"admin(User|Password)\\s*:",
|
|
39
|
+
"default(User|Pass)\\s*:",
|
|
40
|
+
"root(User|Password)\\s*:",
|
|
41
|
+
"service(User|Account)\\s*:"
|
|
42
|
+
],
|
|
43
|
+
"docker": [
|
|
44
|
+
"ENV\\s+.*(USER|USERNAME|_ROOT_USERNAME)\\s*=\\s*(admin|root|sa)",
|
|
45
|
+
"POSTGRES_USER\\s*=\\s*(postgres|admin|root)",
|
|
46
|
+
"MONGO_INITDB_ROOT_USERNAME\\s*=\\s*(root|admin)",
|
|
47
|
+
"MYSQL_USER\\s*=\\s*(root|admin|mysql)",
|
|
48
|
+
"REDIS_USER\\s*=\\s*(redis|admin|root)"
|
|
49
|
+
],
|
|
50
|
+
"kubernetes": [
|
|
51
|
+
"serviceAccount:\\s*default",
|
|
52
|
+
"user:\\s*(admin|root|sa|test|guest)",
|
|
53
|
+
"username:\\s*(admin|root|sa|test|guest)"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"docPatterns": [
|
|
57
|
+
"login\\s*[:=]\\s*(admin|root|sa|test|guest)",
|
|
58
|
+
"user\\s*[:=]\\s*(admin|root|sa|test|guest)",
|
|
59
|
+
"username\\s*[:=]\\s*(admin|root|sa|test|guest)",
|
|
60
|
+
"password\\s*[:=]\\s*(admin|root|sa|test|guest|password|123456)"
|
|
61
|
+
],
|
|
62
|
+
"passwordSmells": [
|
|
63
|
+
"password", "123456", "admin", "Admin@123", "Password1", "changeme",
|
|
64
|
+
"default", "qwerty", "letmein", "welcome", "secret", "pass123",
|
|
65
|
+
"root", "toor", "administrator", "guest"
|
|
66
|
+
],
|
|
67
|
+
"configFilePatterns": [
|
|
68
|
+
"database\\.(username|user)\\s*=\\s*(admin|root|sa)",
|
|
69
|
+
"db\\.(username|user)\\s*=\\s*(admin|root|sa)",
|
|
70
|
+
"auth\\.(username|user)\\s*=\\s*(admin|root|sa)",
|
|
71
|
+
"admin\\.(username|user)\\s*=\\s*",
|
|
72
|
+
"spring\\.datasource\\.username\\s*=\\s*(admin|root|sa)"
|
|
73
|
+
],
|
|
74
|
+
"policy": {
|
|
75
|
+
"requirePerUserAccount": true,
|
|
76
|
+
"requireInitialPasswordChange": true,
|
|
77
|
+
"forbidWellKnownServiceAccountsInAppDB": true,
|
|
78
|
+
"allowOnlyInEphemeralTests": true,
|
|
79
|
+
"mustDisableBuiltInsOnInfra": true
|
|
80
|
+
},
|
|
81
|
+
"allowlist": {
|
|
82
|
+
"paths": [
|
|
83
|
+
"test/", "tests/", "__tests__/", "e2e/", "playground/",
|
|
84
|
+
"local-dev/", "demo/", "example/", "mock/", "fixture/",
|
|
85
|
+
"spec/", ".spec.", ".test."
|
|
86
|
+
],
|
|
87
|
+
"notes": "Vẫn cảnh báo nếu xuất hiện mật khẩu mặc định; cho phép username cấm chỉ khi data giả lập không public và không nối vào môi trường thật."
|
|
88
|
+
},
|
|
89
|
+
"thresholds": {
|
|
90
|
+
"maxFindings": 0,
|
|
91
|
+
"maxInAllowedPaths": 2,
|
|
92
|
+
"maxPasswordSmells": 0
|
|
93
|
+
},
|
|
94
|
+
"exemptions": {
|
|
95
|
+
"testDirectories": ["test", "tests", "__tests__", "e2e", "spec"],
|
|
96
|
+
"configFiles": ["jest.config", "test.config", "local.config"],
|
|
97
|
+
"allowTestData": true,
|
|
98
|
+
"allowDocumentationExamples": false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S056 Main Analyzer - Protect against Log Injection attacks
|
|
3
|
+
* Primary: Symbol-based analysis (when available)
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
* Command: node cli.js --rule=S056 --input=examples/rule-test-fixtures/rules/S056_log_injection_protection --engine=heuristic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const S056SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
+
const S056RegexBasedAnalyzer = require("./regex-based-analyzer.js");
|
|
10
|
+
|
|
11
|
+
class S056Analyzer {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
14
|
+
console.log(`🔧 [S056] Constructor called with options:`, !!options);
|
|
15
|
+
console.log(
|
|
16
|
+
`🔧 [S056] Options type:`,
|
|
17
|
+
typeof options,
|
|
18
|
+
Object.keys(options || {})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.ruleId = "S056";
|
|
23
|
+
this.ruleName = "Protect against Log Injection attacks";
|
|
24
|
+
this.description =
|
|
25
|
+
"Protect against Log Injection attacks. Log injection occurs when user-controlled data is written to log files without proper sanitization, potentially allowing attackers to manipulate log entries, inject malicious content, or exploit log processing systems.";
|
|
26
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
27
|
+
this.verbose = options.verbose || false;
|
|
28
|
+
|
|
29
|
+
// Configuration
|
|
30
|
+
this.config = {
|
|
31
|
+
useSymbolBased: true, // Re-enabled with ts-morph API fixes
|
|
32
|
+
fallbackToRegex: true, // Secondary approach
|
|
33
|
+
regexBasedOnly: false, // Now we can use symbol analysis again
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Initialize analyzers
|
|
37
|
+
try {
|
|
38
|
+
this.symbolAnalyzer = new S056SymbolBasedAnalyzer(this.semanticEngine);
|
|
39
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
40
|
+
console.log(`🔧 [S056] Symbol analyzer created successfully`);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`🔧 [S056] Error creating symbol analyzer:`, error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
this.regexAnalyzer = new S056RegexBasedAnalyzer(this.semanticEngine);
|
|
48
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
49
|
+
console.log(`🔧 [S056] Regex analyzer created successfully`);
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`🔧 [S056] Error creating regex analyzer:`, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize analyzer with semantic engine
|
|
58
|
+
*/
|
|
59
|
+
async initialize(semanticEngine) {
|
|
60
|
+
this.semanticEngine = semanticEngine;
|
|
61
|
+
|
|
62
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
63
|
+
console.log(`🔧 [S056] Main analyzer initializing...`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialize both analyzers
|
|
67
|
+
if (this.symbolAnalyzer) {
|
|
68
|
+
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
69
|
+
}
|
|
70
|
+
if (this.regexAnalyzer) {
|
|
71
|
+
await this.regexAnalyzer.initialize?.(semanticEngine);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Clean up if needed
|
|
75
|
+
if (this.regexAnalyzer) {
|
|
76
|
+
this.regexAnalyzer.cleanup?.();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
80
|
+
console.log(`🔧 [S056] Main analyzer initialized successfully`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Single file analysis method for testing
|
|
86
|
+
*/
|
|
87
|
+
analyzeSingle(filePath, options = {}) {
|
|
88
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
89
|
+
console.log(`📊 [S056] analyzeSingle() called for: ${filePath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Return result using same format as analyze method
|
|
93
|
+
return this.analyze([filePath], "typescript", options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async analyze(files, language, options = {}) {
|
|
97
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
98
|
+
console.log(
|
|
99
|
+
`🔧 [S056] analyze() method called with ${files.length} files, language: ${language}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const violations = [];
|
|
104
|
+
|
|
105
|
+
for (const filePath of files) {
|
|
106
|
+
try {
|
|
107
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
108
|
+
console.log(`🔧 [S056] Processing file: ${filePath}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
112
|
+
violations.push(...fileViolations);
|
|
113
|
+
|
|
114
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
115
|
+
console.log(
|
|
116
|
+
`🔧 [S056] File ${filePath}: Found ${fileViolations.length} violations`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(
|
|
121
|
+
`⚠ [S056] Analysis failed for ${filePath}:`,
|
|
122
|
+
error.message
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
128
|
+
console.log(`🔧 [S056] Total violations found: ${violations.length}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return violations;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async analyzeFile(filePath, options = {}) {
|
|
135
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
136
|
+
console.log(`🔍 [S056] analyzeFile() called for: ${filePath}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Create a Map to track unique violations and prevent duplicates
|
|
140
|
+
const violationMap = new Map();
|
|
141
|
+
|
|
142
|
+
// 1. Try Symbol-based analysis first (primary)
|
|
143
|
+
if (
|
|
144
|
+
this.config.useSymbolBased &&
|
|
145
|
+
this.semanticEngine?.project &&
|
|
146
|
+
this.semanticEngine?.initialized
|
|
147
|
+
) {
|
|
148
|
+
try {
|
|
149
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
150
|
+
console.log(`🔧 [S056] Trying symbol-based analysis...`);
|
|
151
|
+
}
|
|
152
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
153
|
+
if (sourceFile) {
|
|
154
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
155
|
+
console.log(`🔧 [S056] Source file found, analyzing...`);
|
|
156
|
+
}
|
|
157
|
+
const symbolViolations = await this.symbolAnalyzer.analyze(
|
|
158
|
+
sourceFile,
|
|
159
|
+
filePath
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Add to violation map with deduplication
|
|
163
|
+
symbolViolations.forEach((violation) => {
|
|
164
|
+
const key = `${violation.line}:${violation.column}:${violation.message}`;
|
|
165
|
+
if (!violationMap.has(key)) {
|
|
166
|
+
violationMap.set(key, violation);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
171
|
+
console.log(
|
|
172
|
+
`🔧 [S056] Symbol analysis completed: ${symbolViolations.length} violations`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
177
|
+
console.log(`🔧 [S056] Source file not found, falling back...`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn(`⚠ [S056] Symbol analysis failed:`, error.message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2. Try Regex-based analysis (fallback or additional)
|
|
186
|
+
if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
|
|
187
|
+
try {
|
|
188
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
189
|
+
console.log(`🔧 [S056] Trying regex-based analysis...`);
|
|
190
|
+
}
|
|
191
|
+
const regexViolations = await this.regexAnalyzer.analyze(filePath);
|
|
192
|
+
|
|
193
|
+
// Add to violation map with deduplication
|
|
194
|
+
regexViolations.forEach((violation) => {
|
|
195
|
+
const key = `${violation.line}:${violation.column}:${violation.message}`;
|
|
196
|
+
if (!violationMap.has(key)) {
|
|
197
|
+
violationMap.set(key, violation);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
202
|
+
console.log(
|
|
203
|
+
`🔧 [S056] Regex analysis completed: ${regexViolations.length} violations`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.warn(`⚠ [S056] Regex analysis failed:`, error.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Convert Map values to array and add filePath to each violation
|
|
212
|
+
const finalViolations = Array.from(violationMap.values()).map(
|
|
213
|
+
(violation) => ({
|
|
214
|
+
...violation,
|
|
215
|
+
filePath: filePath,
|
|
216
|
+
file: filePath, // Also add 'file' for compatibility
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
221
|
+
console.log(
|
|
222
|
+
`🔧 [S056] File analysis completed: ${finalViolations.length} unique violations`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return finalViolations;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Clean up resources
|
|
231
|
+
*/
|
|
232
|
+
cleanup() {
|
|
233
|
+
if (this.symbolAnalyzer?.cleanup) {
|
|
234
|
+
this.symbolAnalyzer.cleanup();
|
|
235
|
+
}
|
|
236
|
+
if (this.regexAnalyzer?.cleanup) {
|
|
237
|
+
this.regexAnalyzer.cleanup();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = S056Analyzer;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rule": {
|
|
3
|
+
"id": "S056",
|
|
4
|
+
"name": "Protect against Log Injection attacks",
|
|
5
|
+
"description": "Protect against Log Injection attacks. Log injection occurs when user-controlled data is written to log files without proper sanitization, potentially allowing attackers to manipulate log entries, inject malicious content, or exploit log processing systems.",
|
|
6
|
+
"category": "security",
|
|
7
|
+
"severity": "error",
|
|
8
|
+
"languages": ["typescript", "javascript"],
|
|
9
|
+
"frameworks": ["express", "nestjs", "node"],
|
|
10
|
+
"version": "1.0.0",
|
|
11
|
+
"status": "stable",
|
|
12
|
+
"tags": ["security", "logging", "injection", "owasp", "crlf"],
|
|
13
|
+
"references": [
|
|
14
|
+
"https://owasp.org/www-community/attacks/Log_Injection",
|
|
15
|
+
"https://cwe.mitre.org/data/definitions/117.html",
|
|
16
|
+
"https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html",
|
|
17
|
+
"https://portswigger.net/kb/issues/00200200_log-injection"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"configuration": {
|
|
21
|
+
"enableLogInjectionDetection": true,
|
|
22
|
+
"checkUserInputSources": [
|
|
23
|
+
"req",
|
|
24
|
+
"request",
|
|
25
|
+
"params",
|
|
26
|
+
"query",
|
|
27
|
+
"body",
|
|
28
|
+
"headers",
|
|
29
|
+
"cookies",
|
|
30
|
+
"session"
|
|
31
|
+
],
|
|
32
|
+
"vulnerableLogMethods": [
|
|
33
|
+
"log",
|
|
34
|
+
"info",
|
|
35
|
+
"warn",
|
|
36
|
+
"error",
|
|
37
|
+
"debug",
|
|
38
|
+
"trace",
|
|
39
|
+
"write",
|
|
40
|
+
"writeSync"
|
|
41
|
+
],
|
|
42
|
+
"vulnerableLogLibraries": [
|
|
43
|
+
"winston",
|
|
44
|
+
"bunyan",
|
|
45
|
+
"pino",
|
|
46
|
+
"log4js",
|
|
47
|
+
"console",
|
|
48
|
+
"morgan",
|
|
49
|
+
"debug"
|
|
50
|
+
],
|
|
51
|
+
"dangerousCharacters": [
|
|
52
|
+
"\\r",
|
|
53
|
+
"\\n",
|
|
54
|
+
"\\r\\n",
|
|
55
|
+
"\\u000a",
|
|
56
|
+
"\\u000d",
|
|
57
|
+
"%0a",
|
|
58
|
+
"%0d",
|
|
59
|
+
"\\x0a",
|
|
60
|
+
"\\x0d"
|
|
61
|
+
],
|
|
62
|
+
"secureLogPatterns": [
|
|
63
|
+
"sanitize",
|
|
64
|
+
"escape",
|
|
65
|
+
"clean",
|
|
66
|
+
"filter",
|
|
67
|
+
"validate",
|
|
68
|
+
"replace",
|
|
69
|
+
"strip"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"examples": {
|
|
73
|
+
"violations": [
|
|
74
|
+
{
|
|
75
|
+
"description": "Direct user input in log message",
|
|
76
|
+
"code": "logger.info('User login: ' + req.body.username);"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"description": "Template literal with user input",
|
|
80
|
+
"code": "console.log(`User ${req.query.user} attempted login`);"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"description": "User input containing CRLF in log",
|
|
84
|
+
"code": "winston.error('Authentication failed for: ' + req.headers['user-agent']);"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"description": "Session data in log message",
|
|
88
|
+
"code": "logger.debug('Session: ' + JSON.stringify(req.session));"
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"fixes": [
|
|
92
|
+
{
|
|
93
|
+
"description": "Sanitize user input before logging",
|
|
94
|
+
"code": "const sanitizedUser = sanitize(req.body.username);\nlogger.info('User login: ' + sanitizedUser);"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"description": "Use structured logging with safe values",
|
|
98
|
+
"code": "logger.info('User login', { username: sanitize(req.body.username) });"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"description": "Filter dangerous characters",
|
|
102
|
+
"code": "const cleanInput = req.query.user.replace(/[\\r\\n]/g, '_');\nconsole.log(`User ${cleanInput} attempted login`);"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"description": "Validate input before logging",
|
|
106
|
+
"code": "if (validateInput(req.headers['user-agent'])) {\n winston.error('Authentication failed for: ' + req.headers['user-agent']);\n}"
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"testing": {
|
|
111
|
+
"testCases": [
|
|
112
|
+
{
|
|
113
|
+
"name": "log_injection_direct_input",
|
|
114
|
+
"type": "violation",
|
|
115
|
+
"description": "Direct user input in log message"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "log_injection_template_literal",
|
|
119
|
+
"type": "violation",
|
|
120
|
+
"description": "Template literal with user input"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"name": "log_injection_crlf",
|
|
124
|
+
"type": "violation",
|
|
125
|
+
"description": "User input containing CRLF characters"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "log_injection_json_stringify",
|
|
129
|
+
"type": "violation",
|
|
130
|
+
"description": "JSON.stringify with user input"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "secure_log_sanitized",
|
|
134
|
+
"type": "clean",
|
|
135
|
+
"description": "Sanitized user input in log"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "secure_log_structured",
|
|
139
|
+
"type": "clean",
|
|
140
|
+
"description": "Structured logging with safe values"
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
"performance": {
|
|
145
|
+
"complexity": "O(n)",
|
|
146
|
+
"description": "Linear complexity based on number of function calls and expressions in the source code"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S056 Regex-Based Analyzer - Protect against Log Injection attacks
|
|
3
|
+
* Uses regular expressions for pattern-based analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
|
|
8
|
+
class S056RegexBasedAnalyzer {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.ruleId = "S056";
|
|
11
|
+
this.category = "security";
|
|
12
|
+
|
|
13
|
+
// Regex patterns for detecting log injection vulnerabilities
|
|
14
|
+
this.patterns = [
|
|
15
|
+
// Direct concatenation with user input
|
|
16
|
+
{
|
|
17
|
+
pattern: /(logger\.|console\.)(log|info|warn|error|debug)\s*\(\s*['"`].*?\+\s*(req\.|request\.)/g,
|
|
18
|
+
message: "Log injection vulnerability: Direct concatenation of user input in log statement"
|
|
19
|
+
},
|
|
20
|
+
// Template literals with user input
|
|
21
|
+
{
|
|
22
|
+
pattern: /(logger\.|console\.)(log|info|warn|error|debug)\s*\(\s*`.*?\$\{.*?(req\.|request\.)/g,
|
|
23
|
+
message: "Log injection vulnerability: Template literal with user input in log statement"
|
|
24
|
+
},
|
|
25
|
+
// Winston/Bunyan/Pino with user input
|
|
26
|
+
{
|
|
27
|
+
pattern: /(winston\.|bunyan\.|pino\.)(log|info|warn|error|debug)\s*\(\s*['"`].*?\+\s*(req\.|request\.)/g,
|
|
28
|
+
message: "Log injection vulnerability: User input concatenated in Winston/Bunyan/Pino log"
|
|
29
|
+
},
|
|
30
|
+
// JSON.stringify with user input in logs
|
|
31
|
+
{
|
|
32
|
+
pattern: /(logger\.|console\.)(log|info|warn|error|debug)\s*\(.*?JSON\.stringify\s*\(\s*(req\.|request\.|session)/g,
|
|
33
|
+
message: "Log injection vulnerability: JSON.stringify with user input in log statement"
|
|
34
|
+
},
|
|
35
|
+
// User input sources in log calls
|
|
36
|
+
{
|
|
37
|
+
pattern: /(logger\.|console\.|winston\.|bunyan\.|pino\.)(log|info|warn|error|debug).*?(req\.body|req\.query|req\.params|req\.headers|req\.cookies|req\.session)/g,
|
|
38
|
+
message: "Log injection vulnerability: User input source used in log statement without sanitization"
|
|
39
|
+
},
|
|
40
|
+
// CRLF injection patterns
|
|
41
|
+
{
|
|
42
|
+
pattern: /(logger\.|console\.)(log|info|warn|error|debug).*?(\\r|\\n|%0a|%0d)/g,
|
|
43
|
+
message: "Log injection vulnerability: CRLF characters detected in log statement"
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Patterns that indicate secure usage (to reduce false positives)
|
|
48
|
+
this.securePatterns = [
|
|
49
|
+
/sanitize\s*\(/g,
|
|
50
|
+
/escape\s*\(/g,
|
|
51
|
+
/clean\s*\(/g,
|
|
52
|
+
/filter\s*\(/g,
|
|
53
|
+
/validate\s*\(/g,
|
|
54
|
+
/replace\s*\(/g,
|
|
55
|
+
/strip\s*\(/g
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async analyze(filePath) {
|
|
60
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
61
|
+
console.log(`🔍 [${this.ruleId}] Regex: Starting analysis for ${filePath}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const fileContent = fs.readFileSync(filePath, "utf8");
|
|
66
|
+
const violations = [];
|
|
67
|
+
const lines = fileContent.split('\n');
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < lines.length; i++) {
|
|
70
|
+
const line = lines[i];
|
|
71
|
+
const lineNumber = i + 1;
|
|
72
|
+
|
|
73
|
+
// Skip if line contains secure patterns
|
|
74
|
+
if (this.hasSecurePattern(line)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check each vulnerability pattern
|
|
79
|
+
for (const { pattern, message } of this.patterns) {
|
|
80
|
+
const matches = [...line.matchAll(pattern)];
|
|
81
|
+
|
|
82
|
+
for (const match of matches) {
|
|
83
|
+
const column = match.index + 1;
|
|
84
|
+
|
|
85
|
+
violations.push({
|
|
86
|
+
ruleId: this.ruleId,
|
|
87
|
+
message: message,
|
|
88
|
+
line: lineNumber,
|
|
89
|
+
column: column,
|
|
90
|
+
severity: "error",
|
|
91
|
+
category: this.category,
|
|
92
|
+
code: line.trim()
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
99
|
+
console.log(
|
|
100
|
+
`🔍 [${this.ruleId}] Regex: Analysis completed, ${violations.length} violations found`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return violations;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.warn(`⚠ [${this.ruleId}] Regex analysis failed:`, error.message);
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
hasSecurePattern(line) {
|
|
112
|
+
return this.securePatterns.some(pattern => pattern.test(line));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
cleanup() {
|
|
116
|
+
// Cleanup resources if needed
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = S056RegexBasedAnalyzer;
|