@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.
Files changed (69) hide show
  1. package/config/rules/enhanced-rules-registry.json +101 -17
  2. package/config/rules/rules-registry-generated.json +22 -22
  3. package/origin-rules/security-en.md +351 -338
  4. package/package.json +1 -1
  5. package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
  6. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
  7. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
  8. package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
  9. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
  10. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
  11. package/rules/security/S003_open_redirect_protection/README.md +371 -0
  12. package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
  13. package/rules/security/S003_open_redirect_protection/config.json +58 -0
  14. package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
  15. package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
  16. package/rules/security/S004_sensitive_data_logging/config.json +62 -0
  17. package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
  18. package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
  19. package/rules/security/S005_no_origin_auth/config.json +28 -67
  20. package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
  21. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
  22. package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
  23. package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
  24. package/rules/security/S012_hardcoded_secrets/config.json +75 -0
  25. package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
  26. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
  27. package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
  28. package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
  29. package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
  30. package/rules/security/S019_smtp_injection_protection/config.json +35 -0
  31. package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
  32. package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
  33. package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
  34. package/rules/security/S022_escape_output_context/README.md +254 -0
  35. package/rules/security/S022_escape_output_context/analyzer.js +510 -0
  36. package/rules/security/S022_escape_output_context/config.json +229 -0
  37. package/rules/security/S023_no_json_injection/analyzer.js +15 -0
  38. package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
  39. package/rules/security/S023_no_json_injection/config.json +133 -0
  40. package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
  41. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
  42. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
  43. package/rules/security/S029_csrf_protection/config.json +127 -0
  44. package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
  45. package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
  46. package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
  47. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
  48. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
  49. package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
  50. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
  51. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
  52. package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
  53. package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
  54. package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
  55. package/rules/security/S040_session_fixation_protection/config.json +20 -0
  56. package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
  57. package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
  58. package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
  59. package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
  60. package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
  61. package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
  62. package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
  63. package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
  64. package/docs/COMMAND-EXAMPLES.md +0 -390
  65. package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
  66. package/docs/FOLDER_STRUCTURE.md +0 -59
  67. package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
  68. package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
  69. 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 secondArg = args[1]; // Cookie name is second argument
418
- if (secondArg && secondArg.getText) {
419
- const text = secondArg.getText();
420
- return text.replace(/['"]/g, ""); // Remove quotes
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
- rule: this.ruleId,
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
- rule: this.ruleId,
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
- rule: this.ruleId,
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
- rule: this.ruleId,
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
+ }