@sun-asterisk/sunlint 1.3.35 → 1.3.37

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 (103) hide show
  1. package/cli.js +33 -0
  2. package/config/rules/enhanced-rules-registry.json +354 -98
  3. package/config/rules/rules-registry-generated.json +197 -171
  4. package/core/architecture-integration.js +115 -17
  5. package/core/cli-action-handler.js +110 -26
  6. package/core/cli-program.js +14 -3
  7. package/core/github-annotate-service.js +62 -0
  8. package/core/impact-integration.js +309 -176
  9. package/core/init-command.js +227 -0
  10. package/core/output-service.js +53 -5
  11. package/core/summary-report-service.js +46 -0
  12. package/core/unified-rule-registry.js +2 -1
  13. package/engines/eslint-engine.js +6 -0
  14. package/engines/impact/core/detectors/database-detector.js +1 -1
  15. package/engines/impact/core/detectors/endpoint-detector.js +1 -1
  16. package/engines/impact/core/report-generator.js +235 -73
  17. package/origin-rules/security-en.md +470 -282
  18. package/package.json +1 -1
  19. package/rules/security/S001_backend_auth_communications/dart/analyzer.js +44 -0
  20. package/rules/security/S001_backend_auth_communications/index.js +87 -0
  21. package/rules/security/S001_backend_auth_communications/typescript/analyzer.js +164 -0
  22. package/rules/security/S002_os_command_injection/dart/analyzer.js +44 -0
  23. package/rules/security/S002_os_command_injection/index.js +87 -0
  24. package/rules/security/S002_os_command_injection/typescript/analyzer.js +194 -0
  25. package/rules/security/S008_svg_content_validation/dart/analyzer.js +44 -0
  26. package/rules/security/S008_svg_content_validation/index.js +87 -0
  27. package/rules/security/S008_svg_content_validation/typescript/analyzer.js +216 -0
  28. package/rules/security/S018_no_sensitive_browser_storage/dart/analyzer.js +44 -0
  29. package/rules/security/S018_no_sensitive_browser_storage/index.js +86 -0
  30. package/rules/security/S018_no_sensitive_browser_storage/typescript/analyzer.js +193 -0
  31. package/rules/security/S021_referrer_policy/dart/analyzer.js +44 -0
  32. package/rules/security/S021_referrer_policy/index.js +86 -0
  33. package/rules/security/S021_referrer_policy/typescript/analyzer.js +183 -0
  34. package/rules/security/S023_no_json_injection/config.json +133 -44
  35. package/rules/security/S023_no_json_injection/dart/analyzer.js +7 -6
  36. package/rules/security/S023_no_json_injection/typescript/analyzer.js +402 -126
  37. package/rules/security/S023_no_json_injection/typescript/ast-analyzer.js +571 -154
  38. package/rules/security/S026_tls_all_connections/config.json +30 -0
  39. package/rules/security/S026_tls_all_connections/typescript/analyzer.js +339 -0
  40. package/rules/security/S027_mtls_certificate_validation/config.json +30 -0
  41. package/rules/security/S027_mtls_certificate_validation/typescript/analyzer.js +225 -0
  42. package/rules/security/S035_separate_app_hostnames/config.json +28 -0
  43. package/rules/security/S035_separate_app_hostnames/typescript/analyzer.js +186 -0
  44. package/rules/security/S036_lfi_rfi_protection/config.json +2 -2
  45. package/rules/security/S039_tls_certificate_validation/config.json +29 -0
  46. package/rules/security/S039_tls_certificate_validation/typescript/analyzer.js +229 -0
  47. package/rules/security/S046_jwt_algorithm_allowlist/config.json +28 -0
  48. package/rules/security/S046_jwt_algorithm_allowlist/dart/analyzer.js +44 -0
  49. package/rules/security/S046_jwt_algorithm_allowlist/index.js +87 -0
  50. package/rules/security/S046_jwt_algorithm_allowlist/typescript/analyzer.js +235 -0
  51. package/rules/security/S047_oauth_pkce_protection/config.json +31 -0
  52. package/rules/security/S047_oauth_pkce_protection/dart/analyzer.js +44 -0
  53. package/rules/security/S047_oauth_pkce_protection/index.js +86 -0
  54. package/rules/security/S047_oauth_pkce_protection/typescript/analyzer.js +78 -0
  55. package/rules/security/S048_oauth_redirect_uri_validation/config.json +30 -0
  56. package/rules/security/S048_oauth_redirect_uri_validation/typescript/analyzer.js +278 -0
  57. package/rules/security/S049_short_validity_tokens/typescript/config.json +10 -3
  58. package/rules/security/S050_reference_tokens_entropy/config.json +28 -0
  59. package/rules/security/S050_reference_tokens_entropy/dart/analyzer.js +45 -0
  60. package/rules/security/S050_reference_tokens_entropy/index.js +86 -0
  61. package/rules/security/S050_reference_tokens_entropy/typescript/analyzer.js +74 -0
  62. package/rules/security/S053_generic_error_messages/config.json +28 -0
  63. package/rules/security/S053_generic_error_messages/dart/analyzer.js +45 -0
  64. package/rules/security/S053_generic_error_messages/index.js +86 -0
  65. package/rules/security/S053_generic_error_messages/typescript/analyzer.js +80 -0
  66. package/rules/security/S055_content_type_validation/typescript/symbol-based-analyzer.js +64 -2
  67. package/rules/security/S059_disable_debug_mode/config.json +28 -0
  68. package/rules/security/S059_disable_debug_mode/dart/analyzer.js +45 -0
  69. package/rules/security/S059_disable_debug_mode/index.js +86 -0
  70. package/rules/security/S059_disable_debug_mode/typescript/analyzer.js +85 -0
  71. package/rules/security/S060_password_minimum_length/config.json +28 -0
  72. package/rules/security/S060_password_minimum_length/dart/analyzer.js +45 -0
  73. package/rules/security/S060_password_minimum_length/index.js +86 -0
  74. package/rules/security/S060_password_minimum_length/typescript/analyzer.js +78 -0
  75. package/rules/security/S026_json_schema_validation/config.json +0 -27
  76. package/rules/security/S026_json_schema_validation/typescript/analyzer.js +0 -251
  77. package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
  78. package/rules/security/S027_no_hardcoded_secrets/typescript/analyzer.js +0 -309
  79. package/rules/security/S027_no_hardcoded_secrets/typescript/categories.json +0 -153
  80. package/rules/security/S035_path_session_cookies/config.json +0 -99
  81. package/rules/security/S035_path_session_cookies/typescript/analyzer.js +0 -316
  82. package/rules/security/S035_path_session_cookies/typescript/regex-based-analyzer.js +0 -724
  83. package/rules/security/S035_path_session_cookies/typescript/symbol-based-analyzer.js +0 -373
  84. package/rules/security/S039_no_session_tokens_in_url/config.json +0 -92
  85. package/rules/security/S039_no_session_tokens_in_url/typescript/analyzer.js +0 -262
  86. package/rules/security/S039_no_session_tokens_in_url/typescript/regex-based-analyzer.js +0 -337
  87. package/rules/security/S039_no_session_tokens_in_url/typescript/symbol-based-analyzer.js +0 -443
  88. package/rules/security/S048_no_current_password_in_reset/config.json +0 -48
  89. package/rules/security/S048_no_current_password_in_reset/typescript/analyzer.js +0 -366
  90. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/dart/analyzer.js +0 -0
  91. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/index.js +0 -0
  92. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/dart/analyzer.js +0 -0
  93. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/index.js +0 -0
  94. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/typescript/categorized-analyzer.js +0 -0
  95. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/dart/analyzer.js +0 -0
  96. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/index.js +0 -0
  97. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/typescript/README.md +0 -0
  98. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/dart/analyzer.js +0 -0
  99. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/index.js +0 -0
  100. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/typescript/README.md +0 -0
  101. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/dart/analyzer.js +0 -0
  102. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/index.js +0 -0
  103. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/typescript/README.md +0 -0
@@ -0,0 +1,235 @@
1
+ /**
2
+ * S046 – Use algorithm allowlist for self-contained tokens
3
+ *
4
+ * Detects JWT/token verification without algorithm restriction:
5
+ * - Missing algorithms option in jwt.verify()
6
+ * - 'none' algorithm allowed
7
+ * - Using jwt.decode() for security-sensitive operations
8
+ * - Algorithm taken from untrusted token header
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ class S046Analyzer {
15
+ constructor() {
16
+ this.ruleId = 'S046';
17
+ this.ruleName = 'Use algorithm allowlist for self-contained tokens';
18
+ this.description = 'Prevent algorithm confusion attacks by restricting token algorithms';
19
+
20
+ // Critical patterns - dangerous practices
21
+ this.criticalPatterns = [
22
+ // 'none' algorithm explicitly allowed
23
+ {
24
+ pattern: /algorithms\s*:\s*\[[^\]]*['"`]none['"`]/gi,
25
+ message: "Algorithm 'none' is explicitly allowed - this disables signature verification",
26
+ type: 'none_algorithm_allowed'
27
+ },
28
+ // Algorithm from token header (algorithm confusion)
29
+ {
30
+ pattern: /(?:header|decoded|payload)\.alg(?:orithm)?(?!\s*===)/gi,
31
+ message: 'Algorithm from token header used without validation - vulnerable to algorithm confusion',
32
+ type: 'header_algorithm_used'
33
+ },
34
+ // jwt.decode for auth decisions
35
+ {
36
+ pattern: /jwt\.decode\s*\([^)]*\)(?:\s*\.|\s*\[)/gi,
37
+ message: 'jwt.decode() used - this does not verify signature. Use jwt.verify() with algorithm allowlist',
38
+ type: 'decode_without_verify'
39
+ }
40
+ ];
41
+
42
+ // Warning patterns - potential issues
43
+ this.warningPatterns = [
44
+ // jwt.verify without algorithms option
45
+ {
46
+ pattern: /jwt\.verify\s*\(\s*\w+\s*,\s*\w+\s*\)(?!\s*\.)/gi,
47
+ message: 'jwt.verify() called without algorithms option - specify allowed algorithms explicitly',
48
+ type: 'verify_no_algorithms'
49
+ },
50
+ // jsonwebtoken verify without third argument
51
+ {
52
+ pattern: /verify\s*\(\s*token\s*,\s*(?:secret|key|publicKey)\s*\)/gi,
53
+ message: 'Token verification without algorithm restriction - add { algorithms: [...] }',
54
+ type: 'verify_missing_options'
55
+ },
56
+ // jose/node-jose without algorithm check
57
+ {
58
+ pattern: /jws\.verify\s*\([^)]*\)(?![^{]*algorithms)/gi,
59
+ message: 'JWS verification may need algorithm validation',
60
+ type: 'jws_no_algorithm'
61
+ }
62
+ ];
63
+
64
+ // Files to analyze (JWT-related)
65
+ this.targetPatterns = [
66
+ /jwt/i,
67
+ /token/i,
68
+ /auth/i,
69
+ /verify/i,
70
+ /jose/i
71
+ ];
72
+
73
+ // Files to skip
74
+ this.skipPatterns = [
75
+ /\.test\./i,
76
+ /\.spec\./i,
77
+ /test\//i,
78
+ /tests\//i,
79
+ /__tests__\//i,
80
+ /node_modules/i,
81
+ /\.md$/i
82
+ ];
83
+ }
84
+
85
+ shouldSkipFile(filePath) {
86
+ return this.skipPatterns.some(pattern => pattern.test(filePath));
87
+ }
88
+
89
+ isJWTRelatedFile(filePath, content) {
90
+ // Check content for JWT patterns
91
+ const jwtIndicators = [
92
+ /jwt\./i,
93
+ /jsonwebtoken/i,
94
+ /jose/i,
95
+ /jws\./i,
96
+ /Bearer\s+/i,
97
+ /algorithms\s*:/i,
98
+ /\.verify\s*\([^)]*token/i
99
+ ];
100
+ return jwtIndicators.some(pattern => pattern.test(content));
101
+ }
102
+
103
+ async analyze(files, language, options = {}) {
104
+ const violations = [];
105
+
106
+ for (const filePath of files) {
107
+ if (this.shouldSkipFile(filePath)) {
108
+ continue;
109
+ }
110
+
111
+ try {
112
+ const content = fs.readFileSync(filePath, 'utf8');
113
+
114
+ // Only analyze JWT-related files
115
+ if (!this.isJWTRelatedFile(filePath, content)) {
116
+ continue;
117
+ }
118
+
119
+ const fileViolations = this.analyzeFile(content, filePath);
120
+ violations.push(...fileViolations);
121
+ } catch (error) {
122
+ if (options.verbose) {
123
+ console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
124
+ }
125
+ }
126
+ }
127
+
128
+ return violations;
129
+ }
130
+
131
+ analyzeFile(content, filePath) {
132
+ const violations = [];
133
+ const lines = content.split('\n');
134
+
135
+ // Check critical patterns (errors)
136
+ for (const { pattern, message, type } of this.criticalPatterns) {
137
+ pattern.lastIndex = 0;
138
+
139
+ let match;
140
+ while ((match = pattern.exec(content)) !== null) {
141
+ const lineNum = this.getLineNumber(content, match.index);
142
+ const line = lines[lineNum - 1];
143
+
144
+ if (this.isComment(line)) {
145
+ continue;
146
+ }
147
+
148
+ if (this.isTestContext(lines, lineNum)) {
149
+ continue;
150
+ }
151
+
152
+ violations.push({
153
+ file: filePath,
154
+ line: lineNum,
155
+ column: this.getColumnNumber(content, match.index),
156
+ message: message,
157
+ severity: 'error',
158
+ ruleId: this.ruleId,
159
+ type: type,
160
+ matchedText: match[0]
161
+ });
162
+ }
163
+ }
164
+
165
+ // Check warning patterns
166
+ for (const { pattern, message, type } of this.warningPatterns) {
167
+ pattern.lastIndex = 0;
168
+
169
+ let match;
170
+ while ((match = pattern.exec(content)) !== null) {
171
+ const lineNum = this.getLineNumber(content, match.index);
172
+ const line = lines[lineNum - 1];
173
+
174
+ if (this.isComment(line)) {
175
+ continue;
176
+ }
177
+
178
+ if (this.isTestContext(lines, lineNum)) {
179
+ continue;
180
+ }
181
+
182
+ // Check if algorithms are specified in options nearby
183
+ if (this.hasAlgorithmNearby(lines, lineNum)) {
184
+ continue;
185
+ }
186
+
187
+ violations.push({
188
+ file: filePath,
189
+ line: lineNum,
190
+ column: this.getColumnNumber(content, match.index),
191
+ message: message,
192
+ severity: 'warning',
193
+ ruleId: this.ruleId,
194
+ type: type,
195
+ matchedText: match[0]
196
+ });
197
+ }
198
+ }
199
+
200
+ return violations;
201
+ }
202
+
203
+ hasAlgorithmNearby(lines, lineNum) {
204
+ const start = Math.max(0, lineNum - 5);
205
+ const end = Math.min(lines.length, lineNum + 5);
206
+ const context = lines.slice(start, end).join('\n');
207
+
208
+ return /algorithms\s*:\s*\[/.test(context);
209
+ }
210
+
211
+ isComment(line) {
212
+ const trimmed = line.trim();
213
+ return trimmed.startsWith('//') || trimmed.startsWith('#') ||
214
+ trimmed.startsWith('/*') || trimmed.startsWith('*');
215
+ }
216
+
217
+ isTestContext(lines, lineNum) {
218
+ const start = Math.max(0, lineNum - 5);
219
+ const end = Math.min(lines.length, lineNum + 2);
220
+ const context = lines.slice(start, end).join('\n').toLowerCase();
221
+
222
+ return /describe\(|it\(|test\(|jest|mocha|mock|stub|fake/i.test(context);
223
+ }
224
+
225
+ getLineNumber(content, index) {
226
+ return content.substring(0, index).split('\n').length;
227
+ }
228
+
229
+ getColumnNumber(content, index) {
230
+ const lastNewline = content.lastIndexOf('\n', index - 1);
231
+ return index - lastNewline;
232
+ }
233
+ }
234
+
235
+ module.exports = S046Analyzer;
@@ -0,0 +1,31 @@
1
+ {
2
+ "id": "S047",
3
+ "name": "Protect OAuth code flow against CSRF attacks",
4
+ "description": "Implement PKCE (Proof Key for Code Exchange) or state parameter validation to protect OAuth authorization code flow against CSRF attacks. Required for public clients, recommended for confidential clients.",
5
+ "category": "security",
6
+ "severity": "high",
7
+ "enabled": true,
8
+ "engines": ["heuristic"],
9
+ "enginePreference": ["heuristic"],
10
+ "tags": ["security", "oauth", "pkce", "csrf", "authorization"],
11
+ "examples": {
12
+ "valid": [
13
+ "// PKCE implementation",
14
+ "const codeVerifier = crypto.randomBytes(32).toString('base64url');",
15
+ "const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');",
16
+ "// State parameter",
17
+ "const state = crypto.randomBytes(32).toString('hex');"
18
+ ],
19
+ "invalid": [
20
+ "// No PKCE or state parameter",
21
+ "const authUrl = `${authServer}/authorize?client_id=${clientId}&redirect_uri=${redirectUri}`;",
22
+ "// Static state value",
23
+ "const state = 'fixed-state-value';"
24
+ ]
25
+ },
26
+ "fixable": false,
27
+ "docs": {
28
+ "description": "This rule ensures OAuth authorization code flow is protected against CSRF attacks using PKCE or state parameter. PKCE is mandatory for public clients (SPAs, mobile apps). Both code_challenge and code_verifier must be cryptographically random. State parameter must be unique per request and validated on callback.",
29
+ "url": "https://datatracker.ietf.org/doc/html/rfc7636"
30
+ }
31
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * S047 Dart Analyzer - OAuth PKCE Protection
3
+ *
4
+ * This is a JS wrapper that delegates to DartAnalyzer binary.
5
+ * Actual implementation: dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart
6
+ *
7
+ * Rule: Protect OAuth code flow against CSRF attacks using PKCE
8
+ */
9
+
10
+ class DartS047Analyzer {
11
+ constructor() {
12
+ this.ruleId = 'S047';
13
+ this.language = 'dart';
14
+ }
15
+
16
+ getMetadata() {
17
+ return {
18
+ ruleId: 'S047',
19
+ name: 'OAuth PKCE Protection',
20
+ language: 'dart',
21
+ delegateTo: 'dart_analyzer',
22
+ description: 'Protect OAuth code flow against CSRF attacks using PKCE or state parameter'
23
+ };
24
+ }
25
+
26
+ getConfig() {
27
+ return {
28
+ checkPkce: true,
29
+ checkStateParameter: true,
30
+ severity: 'high'
31
+ };
32
+ }
33
+
34
+ async analyze(files, language, options) {
35
+ // Delegated to DartAnalyzer binary via heuristic-engine.js
36
+ return [];
37
+ }
38
+
39
+ supportsLanguage(language) {
40
+ return language === 'dart';
41
+ }
42
+ }
43
+
44
+ module.exports = DartS047Analyzer;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * S047 Rule Router - OAuth PKCE Protection
3
+ *
4
+ * Routes analysis to the appropriate language-specific analyzer.
5
+ * Supports: TypeScript, JavaScript, Dart
6
+ *
7
+ * Rule: Protect OAuth code flow against CSRF attacks using PKCE or state parameter
8
+ */
9
+
10
+ const path = require('path');
11
+
12
+ class S047Router {
13
+ constructor() {
14
+ this.analyzers = new Map();
15
+ this.ruleId = 'S047';
16
+ }
17
+
18
+ getAnalyzer(language) {
19
+ const normalizedLang = this.normalizeLanguage(language);
20
+
21
+ if (!this.analyzers.has(normalizedLang)) {
22
+ try {
23
+ const analyzerPath = path.join(__dirname, normalizedLang, 'analyzer.js');
24
+ const AnalyzerClass = require(analyzerPath);
25
+ this.analyzers.set(normalizedLang, new AnalyzerClass());
26
+ } catch (error) {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ return this.analyzers.get(normalizedLang);
32
+ }
33
+
34
+ normalizeLanguage(language) {
35
+ if (typeof language !== 'string') {
36
+ return 'typescript';
37
+ }
38
+ const languageMap = {
39
+ 'typescript': 'typescript',
40
+ 'javascript': 'typescript',
41
+ 'ts': 'typescript',
42
+ 'js': 'typescript',
43
+ 'dart': 'dart'
44
+ };
45
+ return languageMap[language.toLowerCase()] || language.toLowerCase();
46
+ }
47
+
48
+ supportsLanguage(language) {
49
+ if (typeof language !== 'string') return false;
50
+ const supported = ['typescript', 'javascript', 'ts', 'js', 'dart'];
51
+ return supported.includes(language.toLowerCase());
52
+ }
53
+
54
+ getSupportedLanguages() {
55
+ return ['typescript', 'javascript', 'dart'];
56
+ }
57
+
58
+ async analyze(files, language, options = {}) {
59
+ const analyzer = this.getAnalyzer(language);
60
+ if (!analyzer) return [];
61
+ if (typeof analyzer.analyze === 'function') {
62
+ return analyzer.analyze(files, language, options);
63
+ }
64
+ return [];
65
+ }
66
+
67
+ async initialize(semanticEngineOrLanguage = null, semanticEngine = null) {
68
+ let engine = semanticEngine;
69
+ let lang = null;
70
+
71
+ if (typeof semanticEngineOrLanguage === 'string') {
72
+ lang = semanticEngineOrLanguage;
73
+ } else if (semanticEngineOrLanguage && typeof semanticEngineOrLanguage === 'object') {
74
+ engine = semanticEngineOrLanguage;
75
+ }
76
+
77
+ if (lang) {
78
+ const analyzer = this.getAnalyzer(lang);
79
+ if (analyzer && typeof analyzer.initialize === 'function') {
80
+ await analyzer.initialize(engine);
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ module.exports = new S047Router();
@@ -0,0 +1,78 @@
1
+ /**
2
+ * S047 – Protect OAuth code flow against CSRF attacks
3
+ *
4
+ * Detects OAuth implementations without PKCE or state parameter:
5
+ * - Authorization URLs without code_challenge
6
+ * - Missing state parameter validation
7
+ * - Static/hardcoded state values
8
+ */
9
+
10
+ const fs = require('fs');
11
+
12
+ class S047Analyzer {
13
+ constructor() {
14
+ this.ruleId = 'S047';
15
+ this.ruleName = 'Protect OAuth code flow against CSRF attacks';
16
+ this.description = 'Implement PKCE or state parameter validation for OAuth';
17
+
18
+ this.warningPatterns = [
19
+ // OAuth URL without code_challenge or state
20
+ {
21
+ pattern: /authorize\?[^"'`]*client_id(?![^"'`]*(?:code_challenge|state))/gi,
22
+ message: 'OAuth authorize URL missing PKCE code_challenge or state parameter',
23
+ type: 'missing_pkce_state'
24
+ },
25
+ // Static state values
26
+ {
27
+ pattern: /state\s*[=:]\s*['"`][a-zA-Z0-9_-]{1,20}['"`](?!\s*\|\|)/gi,
28
+ message: 'OAuth state parameter appears hardcoded - use cryptographically random value',
29
+ type: 'static_state'
30
+ }
31
+ ];
32
+
33
+ this.skipPatterns = [
34
+ /\.test\./i, /\.spec\./i, /test\//i, /__tests__\//i, /node_modules/i
35
+ ];
36
+ }
37
+
38
+ shouldSkipFile(filePath) {
39
+ return this.skipPatterns.some(p => p.test(filePath));
40
+ }
41
+
42
+ isOAuthFile(content) {
43
+ return /oauth|authorize|client_id|redirect_uri/i.test(content);
44
+ }
45
+
46
+ async analyze(files, language, options = {}) {
47
+ const violations = [];
48
+ for (const filePath of files) {
49
+ if (this.shouldSkipFile(filePath)) continue;
50
+ try {
51
+ const content = fs.readFileSync(filePath, 'utf8');
52
+ if (!this.isOAuthFile(content)) continue;
53
+ violations.push(...this.analyzeFile(content, filePath));
54
+ } catch (e) { /* skip */ }
55
+ }
56
+ return violations;
57
+ }
58
+
59
+ analyzeFile(content, filePath) {
60
+ const violations = [];
61
+ const lines = content.split('\n');
62
+
63
+ for (const { pattern, message, type } of this.warningPatterns) {
64
+ pattern.lastIndex = 0;
65
+ let match;
66
+ while ((match = pattern.exec(content)) !== null) {
67
+ const lineNum = content.substring(0, match.index).split('\n').length;
68
+ violations.push({
69
+ file: filePath, line: lineNum, column: 1,
70
+ message, severity: 'warning', ruleId: this.ruleId, type
71
+ });
72
+ }
73
+ }
74
+ return violations;
75
+ }
76
+ }
77
+
78
+ module.exports = S047Analyzer;
@@ -0,0 +1,30 @@
1
+ {
2
+ "id": "S048",
3
+ "name": "Validate OAuth redirect URIs with exact string comparison",
4
+ "description": "Prevent OAuth redirect attacks by validating redirect URIs against a client-specific allowlist using exact string comparison, not pattern matching. Each OAuth client must have pre-registered URIs validated during authorization.",
5
+ "category": "security",
6
+ "severity": "critical",
7
+ "enabled": true,
8
+ "engines": ["heuristic"],
9
+ "enginePreference": ["heuristic"],
10
+ "tags": ["security", "oauth", "redirect", "validation", "authorization"],
11
+ "examples": {
12
+ "valid": [
13
+ "// Exact match against pre-registered URIs",
14
+ "if (registeredUris.includes(redirectUri)) { allow(); }",
15
+ "const isValid = clientConfig.redirectUris.some(uri => uri === requestedUri);"
16
+ ],
17
+ "invalid": [
18
+ "// Wildcard redirect (dangerous)",
19
+ "if (redirectUri.startsWith('https://example.com')) { allow(); }",
20
+ "if (redirectUri.includes('example.com')) { allow(); }",
21
+ "// Pattern matching (dangerous)",
22
+ "if (/^https:\\/\\/.*\\.example\\.com/.test(redirectUri)) { allow(); }"
23
+ ]
24
+ },
25
+ "fixable": false,
26
+ "docs": {
27
+ "description": "This rule ensures OAuth redirect URIs are validated using exact string comparison against pre-registered URIs. DO NOT allow wildcard redirects (*.example.com), partial matching, or pattern matching. Each OAuth client should have its own allowlist of redirect URIs registered during client creation. This prevents subdomain takeover attacks, path traversal in redirects, and parameter injection.",
28
+ "url": "https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/"
29
+ }
30
+ }