@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
|
@@ -58,6 +58,21 @@ class S031RegexBasedAnalyzer {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Check if file should be skipped (test files)
|
|
63
|
+
*/
|
|
64
|
+
shouldSkipFile(filePath) {
|
|
65
|
+
const testPatterns = [
|
|
66
|
+
/\.test\.(ts|tsx|js|jsx)$/,
|
|
67
|
+
/\.spec\.(ts|tsx|js|jsx)$/,
|
|
68
|
+
/__tests__\//,
|
|
69
|
+
/__mocks__\//,
|
|
70
|
+
/\/tests?\//,
|
|
71
|
+
/\/fixtures?\//,
|
|
72
|
+
];
|
|
73
|
+
return testPatterns.some((pattern) => pattern.test(filePath));
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
/**
|
|
62
77
|
* Analyze file content using regex patterns
|
|
63
78
|
|
|
@@ -67,6 +82,14 @@ class S031RegexBasedAnalyzer {
|
|
|
67
82
|
console.log(`🔍 [S031] Regex-based analysis for: ${filePath}`);
|
|
68
83
|
}
|
|
69
84
|
|
|
85
|
+
// Skip test files
|
|
86
|
+
if (this.shouldSkipFile(filePath)) {
|
|
87
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
88
|
+
console.log(`⏭ [S031] Skipping test file: ${filePath}`);
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
70
93
|
let content;
|
|
71
94
|
try {
|
|
72
95
|
content = fs.readFileSync(filePath, "utf8");
|
|
@@ -85,6 +108,9 @@ class S031RegexBasedAnalyzer {
|
|
|
85
108
|
this.checkPattern(pattern, content, lines, violations, filePath);
|
|
86
109
|
}
|
|
87
110
|
|
|
111
|
+
// Check for custom cookie utilities (e.g., StorageUtils.setCookie)
|
|
112
|
+
this.checkCustomCookieUtilities(content, lines, violations, filePath);
|
|
113
|
+
|
|
88
114
|
return violations;
|
|
89
115
|
}
|
|
90
116
|
|
|
@@ -191,6 +217,80 @@ class S031RegexBasedAnalyzer {
|
|
|
191
217
|
severity: "error",
|
|
192
218
|
});
|
|
193
219
|
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check for custom cookie utility functions with variable names
|
|
223
|
+
* Handles cases like: StorageUtils.setCookie(STORAGE_KEY.ACCESS_TOKEN, value)
|
|
224
|
+
*/
|
|
225
|
+
checkCustomCookieUtilities(content, lines, violations, filePath) {
|
|
226
|
+
// Pattern to match custom cookie utilities with variable references
|
|
227
|
+
// Matches: Utils.setCookie(VARIABLE_NAME, value) or Utils.setCookie(VARIABLE_NAME, value, options)
|
|
228
|
+
const customCookiePattern = /(\w+\.setCookie)\s*\(\s*([A-Z_][A-Z0-9_.]*)\s*,\s*([^,)]+)(?:\s*,\s*([^)]*))?\s*\)/gi;
|
|
229
|
+
|
|
230
|
+
let match;
|
|
231
|
+
customCookiePattern.lastIndex = 0;
|
|
232
|
+
|
|
233
|
+
while ((match = customCookiePattern.exec(content)) !== null) {
|
|
234
|
+
const methodCall = match[1]; // e.g., "StorageUtils.setCookie"
|
|
235
|
+
const cookieNameVar = match[2]; // e.g., "STORAGE_KEY.ACCESS_TOKEN"
|
|
236
|
+
const cookieValue = match[3]; // e.g., "response.user?.access_token || ''"
|
|
237
|
+
const cookieOptions = match[4] || ""; // e.g., options object if present
|
|
238
|
+
const matchText = match[0];
|
|
239
|
+
|
|
240
|
+
// Check if this looks like a session cookie based on variable name or value
|
|
241
|
+
if (!this.isSessionCookieLikely(cookieNameVar, cookieValue, matchText)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check if secure flag is present in options
|
|
246
|
+
if (!this.hasSecureFlag(cookieOptions, matchText)) {
|
|
247
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
248
|
+
|
|
249
|
+
// Extract a friendly cookie name from the variable
|
|
250
|
+
const friendlyCookieName = this.extractFriendlyCookieName(cookieNameVar);
|
|
251
|
+
|
|
252
|
+
this.addViolation(
|
|
253
|
+
matchText,
|
|
254
|
+
lineNumber,
|
|
255
|
+
violations,
|
|
256
|
+
`Session cookie from "${friendlyCookieName}" missing Secure flag - add secure flag to options`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check if cookie variable name or value suggests it's a session cookie
|
|
264
|
+
*/
|
|
265
|
+
isSessionCookieLikely(varName, value, matchText) {
|
|
266
|
+
const textToCheck = (varName + " " + value + " " + matchText).toLowerCase();
|
|
267
|
+
|
|
268
|
+
// Check against session indicators
|
|
269
|
+
const isSession = this.sessionIndicators.some((indicator) =>
|
|
270
|
+
textToCheck.includes(indicator.toLowerCase())
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Also check for common patterns like ACCESS_TOKEN, REFRESH_TOKEN, etc.
|
|
274
|
+
const tokenPatterns = [
|
|
275
|
+
/access[_-]?token/i,
|
|
276
|
+
/refresh[_-]?token/i,
|
|
277
|
+
/auth[_-]?token/i,
|
|
278
|
+
/id[_-]?token/i,
|
|
279
|
+
/session/i,
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
return isSession || tokenPatterns.some(pattern => pattern.test(textToCheck));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Extract a friendly cookie name from variable reference
|
|
287
|
+
* e.g., "STORAGE_KEY.ACCESS_TOKEN" -> "ACCESS_TOKEN"
|
|
288
|
+
*/
|
|
289
|
+
extractFriendlyCookieName(varName) {
|
|
290
|
+
// If it has a dot, take the last part
|
|
291
|
+
const parts = varName.split(".");
|
|
292
|
+
return parts[parts.length - 1] || varName;
|
|
293
|
+
}
|
|
194
294
|
}
|
|
195
295
|
|
|
196
296
|
module.exports = S031RegexBasedAnalyzer;
|
|
@@ -31,7 +31,7 @@ class S031SymbolBasedAnalyzer {
|
|
|
31
31
|
this.cookieMethods = [
|
|
32
32
|
"setCookie",
|
|
33
33
|
"cookie",
|
|
34
|
-
"set",
|
|
34
|
+
// Note: "set" removed - too generic, matches localStorage.set(), Map.set(), etc.
|
|
35
35
|
"append",
|
|
36
36
|
"session",
|
|
37
37
|
"setHeader",
|
|
@@ -693,6 +693,13 @@ class S031SymbolBasedAnalyzer {
|
|
|
693
693
|
if (ts.isIdentifier(firstArg)) {
|
|
694
694
|
return firstArg.text;
|
|
695
695
|
}
|
|
696
|
+
// Handle PropertyAccessExpression like STORAGE_KEY.ACCESS_TOKEN
|
|
697
|
+
if (ts.isPropertyAccessExpression(firstArg)) {
|
|
698
|
+
const fullText = firstArg.getText();
|
|
699
|
+
// Extract the last part (e.g., "ACCESS_TOKEN" from "STORAGE_KEY.ACCESS_TOKEN")
|
|
700
|
+
const parts = fullText.split('.');
|
|
701
|
+
return parts[parts.length - 1] || fullText;
|
|
702
|
+
}
|
|
696
703
|
}
|
|
697
704
|
return null;
|
|
698
705
|
}
|
|
@@ -246,9 +246,9 @@ class S032Analyzer {
|
|
|
246
246
|
extractCookieName(message) {
|
|
247
247
|
try {
|
|
248
248
|
const match = message.match(
|
|
249
|
-
/Session cookie "([^"]+)"|useCookie "([^"]+)"|Cookie "([^"]+)"/
|
|
249
|
+
/Session cookie "([^"]+)"|Session cookie from "([^"]+)"|useCookie "([^"]+)"|Cookie "([^"]+)"/
|
|
250
250
|
);
|
|
251
|
-
return match ? match[1] || match[2] || match[3] : "";
|
|
251
|
+
return match ? match[1] || match[2] || match[3] || match[4] : "";
|
|
252
252
|
} catch (error) {
|
|
253
253
|
return "";
|
|
254
254
|
}
|
|
@@ -104,6 +104,21 @@ class S032RegexBasedAnalyzer {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Check if file should be skipped (test files)
|
|
109
|
+
*/
|
|
110
|
+
shouldSkipFile(filePath) {
|
|
111
|
+
const testPatterns = [
|
|
112
|
+
/\.test\.(ts|tsx|js|jsx)$/,
|
|
113
|
+
/\.spec\.(ts|tsx|js|jsx)$/,
|
|
114
|
+
/__tests__\//,
|
|
115
|
+
/__mocks__\//,
|
|
116
|
+
/\/tests?\//,
|
|
117
|
+
/\/fixtures?\//,
|
|
118
|
+
];
|
|
119
|
+
return testPatterns.some((pattern) => pattern.test(filePath));
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
/**
|
|
108
123
|
* Main analysis method
|
|
109
124
|
*/
|
|
@@ -112,6 +127,14 @@ class S032RegexBasedAnalyzer {
|
|
|
112
127
|
console.log(`🔍 [S032] Regex-based analysis for: ${filePath}`);
|
|
113
128
|
}
|
|
114
129
|
|
|
130
|
+
// Skip test files
|
|
131
|
+
if (this.shouldSkipFile(filePath)) {
|
|
132
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
133
|
+
console.log(`⏭ [S032] Skipping test file: ${filePath}`);
|
|
134
|
+
}
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
const violations = [];
|
|
116
139
|
const violationMap = new Map(); // Deduplication map
|
|
117
140
|
|
|
@@ -135,6 +158,9 @@ class S032RegexBasedAnalyzer {
|
|
|
135
158
|
);
|
|
136
159
|
}
|
|
137
160
|
|
|
161
|
+
// Check for custom cookie utilities (e.g., StorageUtils.setCookie)
|
|
162
|
+
this.checkCustomCookieUtilities(content, lines, violations, filePath);
|
|
163
|
+
|
|
138
164
|
// Check for session middleware without cookie config
|
|
139
165
|
// This method is now mainly handled by checkPattern, but keep for edge cases
|
|
140
166
|
} catch (error) {
|
|
@@ -710,6 +736,95 @@ class S032RegexBasedAnalyzer {
|
|
|
710
736
|
|
|
711
737
|
return hasHttpOnly;
|
|
712
738
|
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Check for custom cookie utility functions with variable names
|
|
742
|
+
* Handles cases like: StorageUtils.setCookie(STORAGE_KEY.ACCESS_TOKEN, value)
|
|
743
|
+
*/
|
|
744
|
+
checkCustomCookieUtilities(content, lines, violations, filePath) {
|
|
745
|
+
// Pattern to match custom cookie utilities with variable references
|
|
746
|
+
// Matches: Utils.setCookie(VARIABLE_NAME, value) or Utils.setCookie(VARIABLE_NAME, value, options)
|
|
747
|
+
const customCookiePattern = /(\w+\.setCookie)\s*\(\s*([A-Z_][A-Z0-9_.]*)\s*,\s*([^,)]+)(?:\s*,\s*([^)]*))?\s*\)/gi;
|
|
748
|
+
|
|
749
|
+
let match;
|
|
750
|
+
customCookiePattern.lastIndex = 0;
|
|
751
|
+
|
|
752
|
+
while ((match = customCookiePattern.exec(content)) !== null) {
|
|
753
|
+
const methodCall = match[1]; // e.g., "StorageUtils.setCookie"
|
|
754
|
+
const cookieNameVar = match[2]; // e.g., "STORAGE_KEY.ACCESS_TOKEN"
|
|
755
|
+
const cookieValue = match[3]; // e.g., "response.user?.access_token || ''"
|
|
756
|
+
const cookieOptions = match[4] || ""; // e.g., options object if present
|
|
757
|
+
const matchText = match[0];
|
|
758
|
+
|
|
759
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
760
|
+
console.log(
|
|
761
|
+
`🔍 [S032] Custom cookie utility: ${methodCall}(${cookieNameVar}, ...)`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Check if this looks like a session cookie based on variable name or value
|
|
766
|
+
if (!this.isSessionCookieLikely(cookieNameVar, cookieValue, matchText)) {
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Check if httpOnly flag is present in options
|
|
771
|
+
if (!this.hasHttpOnlyFlag(cookieOptions)) {
|
|
772
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
773
|
+
|
|
774
|
+
// Extract a friendly cookie name from the variable
|
|
775
|
+
const friendlyCookieName = this.extractFriendlyCookieName(cookieNameVar);
|
|
776
|
+
|
|
777
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
778
|
+
console.log(
|
|
779
|
+
`🔍 [S032] ⚠️ VIOLATION: Custom cookie "${friendlyCookieName}" missing httpOnly at line ${lineNumber}`
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
violations.push({
|
|
784
|
+
ruleId: this.ruleId,
|
|
785
|
+
source: filePath,
|
|
786
|
+
category: this.category,
|
|
787
|
+
line: lineNumber,
|
|
788
|
+
column: 1,
|
|
789
|
+
message: `Insecure session cookie: Session cookie from "${friendlyCookieName}" missing HttpOnly attribute - add httpOnly flag to options`,
|
|
790
|
+
severity: "error",
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Check if cookie variable name or value suggests it's a session cookie
|
|
798
|
+
*/
|
|
799
|
+
isSessionCookieLikely(varName, value, matchText) {
|
|
800
|
+
const textToCheck = (varName + " " + value + " " + matchText).toLowerCase();
|
|
801
|
+
|
|
802
|
+
// Check against session indicators
|
|
803
|
+
const isSession = this.sessionIndicators.some((indicator) =>
|
|
804
|
+
textToCheck.includes(indicator.toLowerCase())
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
// Also check for common patterns like ACCESS_TOKEN, REFRESH_TOKEN, etc.
|
|
808
|
+
const tokenPatterns = [
|
|
809
|
+
/access[_-]?token/i,
|
|
810
|
+
/refresh[_-]?token/i,
|
|
811
|
+
/auth[_-]?token/i,
|
|
812
|
+
/id[_-]?token/i,
|
|
813
|
+
/session/i,
|
|
814
|
+
];
|
|
815
|
+
|
|
816
|
+
return isSession || tokenPatterns.some(pattern => pattern.test(textToCheck));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Extract a friendly cookie name from variable reference
|
|
821
|
+
* e.g., "STORAGE_KEY.ACCESS_TOKEN" -> "ACCESS_TOKEN"
|
|
822
|
+
*/
|
|
823
|
+
extractFriendlyCookieName(varName) {
|
|
824
|
+
// If it has a dot, take the last part
|
|
825
|
+
const parts = varName.split(".");
|
|
826
|
+
return parts[parts.length - 1] || varName;
|
|
827
|
+
}
|
|
713
828
|
}
|
|
714
829
|
|
|
715
830
|
module.exports = S032RegexBasedAnalyzer;
|
|
@@ -50,7 +50,7 @@ class S032SymbolBasedAnalyzer {
|
|
|
50
50
|
this.cookieMethods = [
|
|
51
51
|
"setCookie",
|
|
52
52
|
"cookie",
|
|
53
|
-
"set",
|
|
53
|
+
// Note: "set" removed - too generic, matches localStorage.set(), Map.set(), etc.
|
|
54
54
|
"append",
|
|
55
55
|
"session",
|
|
56
56
|
"setHeader",
|
|
@@ -412,18 +412,37 @@ class S032SymbolBasedAnalyzer {
|
|
|
412
412
|
if (args && args.length > 0) {
|
|
413
413
|
const methodName = this.getMorphMethodName(callNode);
|
|
414
414
|
|
|
415
|
-
// Handle setCookie(event, "cookieName", "value", options) pattern
|
|
415
|
+
// Handle Nuxt3 setCookie(event, "cookieName", "value", options) pattern
|
|
416
|
+
// Check if first argument looks like an event object (not a cookie name)
|
|
416
417
|
if (methodName === "setCookie" && args.length >= 2) {
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
418
|
+
const firstArg = args[0];
|
|
419
|
+
const firstArgText = firstArg.getText ? firstArg.getText() : "";
|
|
420
|
+
|
|
421
|
+
// If first arg looks like an event (contains 'event', 'req', 'context'),
|
|
422
|
+
// then cookie name is second arg (Nuxt3 pattern)
|
|
423
|
+
if (firstArgText.match(/\bevent\b|\.event\b|\breq\b|\bcontext\b/i)) {
|
|
424
|
+
const secondArg = args[1]; // Cookie name is second argument
|
|
425
|
+
if (secondArg && secondArg.getText) {
|
|
426
|
+
const text = secondArg.getText();
|
|
427
|
+
return text.replace(/['"]/g, ""); // Remove quotes
|
|
428
|
+
}
|
|
421
429
|
}
|
|
422
430
|
}
|
|
423
431
|
|
|
424
432
|
// Handle standard cookie methods (cookieName is first argument)
|
|
433
|
+
// This includes: StorageUtils.setCookie(COOKIE_NAME, value)
|
|
425
434
|
const firstArg = args[0];
|
|
426
435
|
if (firstArg && firstArg.getText) {
|
|
436
|
+
const kind = firstArg.getKindName();
|
|
437
|
+
|
|
438
|
+
// Handle PropertyAccessExpression like STORAGE_KEY.ACCESS_TOKEN
|
|
439
|
+
if (kind === "PropertyAccessExpression") {
|
|
440
|
+
const fullText = firstArg.getText();
|
|
441
|
+
const parts = fullText.split('.');
|
|
442
|
+
return parts[parts.length - 1] || fullText;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Handle Identifier or StringLiteral
|
|
427
446
|
const text = firstArg.getText();
|
|
428
447
|
return text.replace(/['"]/g, ""); // Remove quotes
|
|
429
448
|
}
|
|
@@ -707,7 +726,7 @@ class S032SymbolBasedAnalyzer {
|
|
|
707
726
|
const lineAndChar = sourceFile.getLineAndColumnAtPos(start);
|
|
708
727
|
|
|
709
728
|
violations.push({
|
|
710
|
-
|
|
729
|
+
ruleId: this.ruleId,
|
|
711
730
|
source: sourceFile.getFilePath(),
|
|
712
731
|
category: this.category,
|
|
713
732
|
line: lineAndChar.line,
|
|
@@ -718,7 +737,7 @@ class S032SymbolBasedAnalyzer {
|
|
|
718
737
|
} catch (error) {
|
|
719
738
|
// Fallback violation without line/column info
|
|
720
739
|
violations.push({
|
|
721
|
-
|
|
740
|
+
ruleId: this.ruleId,
|
|
722
741
|
source: sourceFile.getFilePath ? sourceFile.getFilePath() : "unknown",
|
|
723
742
|
category: this.category,
|
|
724
743
|
line: 1,
|
|
@@ -1022,6 +1041,16 @@ class S032SymbolBasedAnalyzer {
|
|
|
1022
1041
|
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
1023
1042
|
return firstArg.text;
|
|
1024
1043
|
}
|
|
1044
|
+
if (ts.isIdentifier(firstArg)) {
|
|
1045
|
+
return firstArg.text;
|
|
1046
|
+
}
|
|
1047
|
+
// Handle PropertyAccessExpression like STORAGE_KEY.ACCESS_TOKEN
|
|
1048
|
+
if (ts.isPropertyAccessExpression(firstArg)) {
|
|
1049
|
+
const fullText = firstArg.getText();
|
|
1050
|
+
// Extract the last part (e.g., "ACCESS_TOKEN" from "STORAGE_KEY.ACCESS_TOKEN")
|
|
1051
|
+
const parts = fullText.split('.');
|
|
1052
|
+
return parts[parts.length - 1] || fullText;
|
|
1053
|
+
}
|
|
1025
1054
|
}
|
|
1026
1055
|
} catch (error) {
|
|
1027
1056
|
if (process.env.SUNLINT_DEBUG) {
|
|
@@ -1242,7 +1271,7 @@ class S032SymbolBasedAnalyzer {
|
|
|
1242
1271
|
);
|
|
1243
1272
|
|
|
1244
1273
|
violations.push({
|
|
1245
|
-
|
|
1274
|
+
ruleId: this.ruleId,
|
|
1246
1275
|
source: sourceFile.fileName,
|
|
1247
1276
|
category: this.category,
|
|
1248
1277
|
line: start.line + 1,
|
|
@@ -1253,7 +1282,7 @@ class S032SymbolBasedAnalyzer {
|
|
|
1253
1282
|
} catch (error) {
|
|
1254
1283
|
// Fallback violation
|
|
1255
1284
|
violations.push({
|
|
1256
|
-
|
|
1285
|
+
ruleId: this.ruleId,
|
|
1257
1286
|
source: sourceFile.fileName || "unknown",
|
|
1258
1287
|
category: this.category,
|
|
1259
1288
|
line: 1,
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
class S036Analyzer {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.ruleId = 'S036';
|
|
6
|
+
this.ruleName = 'LFI/RFI Protection';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async analyze(files, language, options = {}) {
|
|
10
|
+
const violations = [];
|
|
11
|
+
|
|
12
|
+
for (const filePath of files) {
|
|
13
|
+
// Skip test files
|
|
14
|
+
if (this.isTestFile(filePath)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
20
|
+
const fileViolations = this.analyzeFile(content, filePath);
|
|
21
|
+
violations.push(...fileViolations);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (options.verbose) {
|
|
24
|
+
console.warn(`S036: Error analyzing ${filePath}:`, error.message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return violations;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
isTestFile(filePath) {
|
|
33
|
+
const testPatterns = [
|
|
34
|
+
/\.test\.(ts|tsx|js|jsx|php|py)$/,
|
|
35
|
+
/\.spec\.(ts|tsx|js|jsx|php|py)$/,
|
|
36
|
+
/__tests__\//,
|
|
37
|
+
/__mocks__\//,
|
|
38
|
+
/\/tests?\//,
|
|
39
|
+
];
|
|
40
|
+
return testPatterns.some(p => p.test(filePath));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
analyzeFile(content, filePath) {
|
|
44
|
+
const violations = [];
|
|
45
|
+
const lines = content.split(/\r?\n/);
|
|
46
|
+
|
|
47
|
+
lines.forEach((line, index) => {
|
|
48
|
+
const lineNumber = index + 1;
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
|
|
51
|
+
// Skip comments
|
|
52
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('/*')) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check for unsafe file operations
|
|
57
|
+
violations.push(...this.checkUnsafeFileOperations(line, lineNumber, filePath));
|
|
58
|
+
|
|
59
|
+
// Check for path traversal
|
|
60
|
+
violations.push(...this.checkPathTraversal(line, lineNumber, filePath));
|
|
61
|
+
|
|
62
|
+
// Check for remote file inclusion
|
|
63
|
+
violations.push(...this.checkRemoteFileInclusion(line, lineNumber, filePath));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
checkUnsafeFileOperations(line, lineNumber, filePath) {
|
|
70
|
+
const violations = [];
|
|
71
|
+
|
|
72
|
+
// Unsafe file read/include patterns with user input
|
|
73
|
+
const unsafePatterns = [
|
|
74
|
+
// JavaScript/TypeScript
|
|
75
|
+
{
|
|
76
|
+
pattern: /(?:fs\.readFileSync|fs\.readFile|require|import)\s*\(\s*[^)]*(?:req\.|request\.|params\.|query\.|body\.)[^)]*\)/gi,
|
|
77
|
+
message: 'Unsafe file operation with user input - validate and use allowlist',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
pattern: /(?:fs\.readFileSync|fs\.readFile|fs\.createReadStream)\s*\(\s*[`'"](.*?)(?:\$\{[^}]+\}|%s|%d)[^`'"]*[`'"]\s*\)/gi,
|
|
81
|
+
message: 'File path uses string interpolation with potential user input - validate path',
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// PHP
|
|
85
|
+
{
|
|
86
|
+
pattern: /(?:include|require|include_once|require_once|fopen|file_get_contents|readfile)\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE|SERVER)/gi,
|
|
87
|
+
message: 'PHP: Direct use of superglobal in file operation - extreme LFI/RFI risk',
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Python
|
|
91
|
+
{
|
|
92
|
+
pattern: /(?:open|exec|execfile|compile|__import__)\s*\(\s*(?:request\.|params\.|args\.|form\.)/gi,
|
|
93
|
+
message: 'Python: File operation with user input - validate and sanitize',
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
unsafePatterns.forEach(({ pattern, message }) => {
|
|
98
|
+
if (pattern.test(line)) {
|
|
99
|
+
violations.push({
|
|
100
|
+
file: filePath,
|
|
101
|
+
line: lineNumber,
|
|
102
|
+
column: 1,
|
|
103
|
+
message: message,
|
|
104
|
+
severity: 2, // error
|
|
105
|
+
ruleId: this.ruleId,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return violations;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
checkPathTraversal(line, lineNumber, filePath) {
|
|
114
|
+
const violations = [];
|
|
115
|
+
|
|
116
|
+
// Skip import/require statements - these are safe module imports
|
|
117
|
+
if (this.isImportStatement(line)) {
|
|
118
|
+
return violations;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Only check path traversal in actual file operations
|
|
122
|
+
// Path traversal patterns in file operations
|
|
123
|
+
const traversalPatterns = [
|
|
124
|
+
{
|
|
125
|
+
// Path traversal in file operations
|
|
126
|
+
pattern: /(?:fs\.readFile|readFile|fopen|file_get_contents|open|sendFile|readFileSync|createReadStream)\s*\([^)]*\.\.\/[^)]*\)/gi,
|
|
127
|
+
message: 'Path traversal in file operation (../) - normalize and validate paths',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
pattern: /(?:fs\.readFile|readFile|fopen|file_get_contents|open|sendFile|readFileSync|createReadStream)\s*\([^)]*\.\.\\[^)]*\)/gi,
|
|
131
|
+
message: 'Path traversal in file operation (..\\) - normalize and validate paths',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
pattern: /(?:fs\.readFile|readFile|fopen|file_get_contents|open|sendFile|readFileSync|createReadStream)\s*\([^)]*\/etc\/passwd[^)]*\)/gi,
|
|
135
|
+
message: 'Suspicious file path (/etc/passwd) - potential LFI attempt',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
pattern: /(?:fs\.readFile|readFile|fopen|file_get_contents|open|sendFile|readFileSync|createReadStream)\s*\([^)]*C:\\Windows\\system32[^)]*\)/gi,
|
|
139
|
+
message: 'Suspicious file path (Windows system32) - potential LFI attempt',
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
traversalPatterns.forEach(({ pattern, message }) => {
|
|
144
|
+
const match = line.match(pattern);
|
|
145
|
+
if (match && !this.isSafeContext(line)) {
|
|
146
|
+
violations.push({
|
|
147
|
+
file: filePath,
|
|
148
|
+
line: lineNumber,
|
|
149
|
+
column: 1,
|
|
150
|
+
message: message,
|
|
151
|
+
severity: 2, // error
|
|
152
|
+
ruleId: this.ruleId,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return violations;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
checkRemoteFileInclusion(line, lineNumber, filePath) {
|
|
161
|
+
const violations = [];
|
|
162
|
+
|
|
163
|
+
// Remote file inclusion patterns
|
|
164
|
+
const rfiPatterns = [
|
|
165
|
+
{
|
|
166
|
+
pattern: /(?:require|import|include|include_once|require_once)\s*\(\s*['"`]https?:\/\//gi,
|
|
167
|
+
message: 'Remote File Inclusion (RFI) detected - never include remote URLs',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
pattern: /(?:fs\.readFile|file_get_contents|fopen|urllib\.request\.urlopen)\s*\(\s*[^)]*(?:http:\/\/|https:\/\/)[^)]*\)/gi,
|
|
171
|
+
message: 'Loading remote file content - validate URL allowlist',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
pattern: /eval\s*\(\s*(?:fs\.readFileSync|file_get_contents|fetch|axios)\s*\(/gi,
|
|
175
|
+
message: 'Eval with file content - extreme code injection risk',
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
rfiPatterns.forEach(({ pattern, message }) => {
|
|
180
|
+
if (pattern.test(line)) {
|
|
181
|
+
violations.push({
|
|
182
|
+
file: filePath,
|
|
183
|
+
line: lineNumber,
|
|
184
|
+
column: 1,
|
|
185
|
+
message: message,
|
|
186
|
+
severity: 2, // error
|
|
187
|
+
ruleId: this.ruleId,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return violations;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
isImportStatement(line) {
|
|
196
|
+
// Check if this is an import/require statement (safe module imports)
|
|
197
|
+
const importPatterns = [
|
|
198
|
+
/^\s*import\s+/,
|
|
199
|
+
/^\s*export\s+.*from\s+/,
|
|
200
|
+
/^\s*const\s+.*=\s*require\s*\(/,
|
|
201
|
+
/^\s*let\s+.*=\s*require\s*\(/,
|
|
202
|
+
/^\s*var\s+.*=\s*require\s*\(/,
|
|
203
|
+
/^\s*import\s*\(/, // dynamic import
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
return importPatterns.some(p => p.test(line));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
isSafeContext(line) {
|
|
210
|
+
// Check if line is in a safe context (e.g., configuration, constants)
|
|
211
|
+
const safePatterns = [
|
|
212
|
+
/const\s+[A-Z_]+\s*=/, // Constants
|
|
213
|
+
/BASE_PATH|ROOT_DIR|PUBLIC_DIR|STATIC_DIR/,
|
|
214
|
+
/path\.join\s*\(\s*__dirname/, // Relative to current dir
|
|
215
|
+
/\.resolve\s*\(/, // path.resolve
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
return safePatterns.some(p => p.test(line));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
cleanup() {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = S036Analyzer;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "S036",
|
|
3
|
+
"name": "LFI and RFI Protection - Prevent Local and Remote File Inclusion",
|
|
4
|
+
"category": "security",
|
|
5
|
+
"description": "S036 - Prevent Local File Inclusion (LFI) and Remote File Inclusion (RFI) vulnerabilities by validating file paths and using allowlists.",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"patterns": {
|
|
9
|
+
"include": ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx", "**/*.php", "**/*.py"],
|
|
10
|
+
"exclude": [
|
|
11
|
+
"**/*.test.*",
|
|
12
|
+
"**/*.spec.*",
|
|
13
|
+
"__tests__/**",
|
|
14
|
+
"__mocks__/**",
|
|
15
|
+
"**/node_modules/**",
|
|
16
|
+
"**/dist/**",
|
|
17
|
+
"**/build/**"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|