@havoc-security/scanner 0.1.0

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 (140) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +22 -0
  3. package/dist/analyzers/AuthorizationCoverageAnalyzer.d.ts +7 -0
  4. package/dist/analyzers/AuthorizationCoverageAnalyzer.d.ts.map +1 -0
  5. package/dist/analyzers/AuthorizationCoverageAnalyzer.js +100 -0
  6. package/dist/analyzers/AuthorizationCoverageAnalyzer.js.map +1 -0
  7. package/dist/analyzers/CredentialExposureAnalyzer.d.ts +11 -0
  8. package/dist/analyzers/CredentialExposureAnalyzer.d.ts.map +1 -0
  9. package/dist/analyzers/CredentialExposureAnalyzer.js +262 -0
  10. package/dist/analyzers/CredentialExposureAnalyzer.js.map +1 -0
  11. package/dist/analyzers/DependencyAuditAnalyzer.d.ts +28 -0
  12. package/dist/analyzers/DependencyAuditAnalyzer.d.ts.map +1 -0
  13. package/dist/analyzers/DependencyAuditAnalyzer.js +107 -0
  14. package/dist/analyzers/DependencyAuditAnalyzer.js.map +1 -0
  15. package/dist/analyzers/EncryptionAnalyzer.d.ts +7 -0
  16. package/dist/analyzers/EncryptionAnalyzer.d.ts.map +1 -0
  17. package/dist/analyzers/EncryptionAnalyzer.js +170 -0
  18. package/dist/analyzers/EncryptionAnalyzer.js.map +1 -0
  19. package/dist/analyzers/FileUploadAnalyzer.d.ts +8 -0
  20. package/dist/analyzers/FileUploadAnalyzer.d.ts.map +1 -0
  21. package/dist/analyzers/FileUploadAnalyzer.js +193 -0
  22. package/dist/analyzers/FileUploadAnalyzer.js.map +1 -0
  23. package/dist/analyzers/IdorAnalyzer.d.ts +7 -0
  24. package/dist/analyzers/IdorAnalyzer.d.ts.map +1 -0
  25. package/dist/analyzers/IdorAnalyzer.js +91 -0
  26. package/dist/analyzers/IdorAnalyzer.js.map +1 -0
  27. package/dist/analyzers/MassAssignmentAnalyzer.d.ts +7 -0
  28. package/dist/analyzers/MassAssignmentAnalyzer.d.ts.map +1 -0
  29. package/dist/analyzers/MassAssignmentAnalyzer.js +90 -0
  30. package/dist/analyzers/MassAssignmentAnalyzer.js.map +1 -0
  31. package/dist/analyzers/PrivilegeEscalationAnalyzer.d.ts +7 -0
  32. package/dist/analyzers/PrivilegeEscalationAnalyzer.d.ts.map +1 -0
  33. package/dist/analyzers/PrivilegeEscalationAnalyzer.js +217 -0
  34. package/dist/analyzers/PrivilegeEscalationAnalyzer.js.map +1 -0
  35. package/dist/analyzers/RateLimitAnalyzer.d.ts +7 -0
  36. package/dist/analyzers/RateLimitAnalyzer.d.ts.map +1 -0
  37. package/dist/analyzers/RateLimitAnalyzer.js +151 -0
  38. package/dist/analyzers/RateLimitAnalyzer.js.map +1 -0
  39. package/dist/analyzers/SessionSecurityAnalyzer.d.ts +10 -0
  40. package/dist/analyzers/SessionSecurityAnalyzer.d.ts.map +1 -0
  41. package/dist/analyzers/SessionSecurityAnalyzer.js +295 -0
  42. package/dist/analyzers/SessionSecurityAnalyzer.js.map +1 -0
  43. package/dist/analyzers/SqlInjectionAnalyzer.d.ts +7 -0
  44. package/dist/analyzers/SqlInjectionAnalyzer.d.ts.map +1 -0
  45. package/dist/analyzers/SqlInjectionAnalyzer.js +77 -0
  46. package/dist/analyzers/SqlInjectionAnalyzer.js.map +1 -0
  47. package/dist/analyzers/XssSurfaceAnalyzer.d.ts +7 -0
  48. package/dist/analyzers/XssSurfaceAnalyzer.d.ts.map +1 -0
  49. package/dist/analyzers/XssSurfaceAnalyzer.js +100 -0
  50. package/dist/analyzers/XssSurfaceAnalyzer.js.map +1 -0
  51. package/dist/analyzers/index.d.ts +13 -0
  52. package/dist/analyzers/index.d.ts.map +1 -0
  53. package/dist/analyzers/index.js +13 -0
  54. package/dist/analyzers/index.js.map +1 -0
  55. package/dist/index.d.ts +17 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +139 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/parsers/PhpParser.d.ts +56 -0
  60. package/dist/parsers/PhpParser.d.ts.map +1 -0
  61. package/dist/parsers/PhpParser.js +193 -0
  62. package/dist/parsers/PhpParser.js.map +1 -0
  63. package/dist/parsers/RouteParser.d.ts +87 -0
  64. package/dist/parsers/RouteParser.d.ts.map +1 -0
  65. package/dist/parsers/RouteParser.js +327 -0
  66. package/dist/parsers/RouteParser.js.map +1 -0
  67. package/dist/rules/index.d.ts +14 -0
  68. package/dist/rules/index.d.ts.map +1 -0
  69. package/dist/rules/index.js +9 -0
  70. package/dist/rules/index.js.map +1 -0
  71. package/dist/types/index.d.ts +137 -0
  72. package/dist/types/index.d.ts.map +1 -0
  73. package/dist/types/index.js +13 -0
  74. package/dist/types/index.js.map +1 -0
  75. package/package.json +30 -0
  76. package/package.json.bak +27 -0
  77. package/src/analyzers/AuthorizationCoverageAnalyzer.ts +213 -0
  78. package/src/analyzers/CredentialExposureAnalyzer.ts +312 -0
  79. package/src/analyzers/DependencyAuditAnalyzer.ts +135 -0
  80. package/src/analyzers/EncryptionAnalyzer.ts +195 -0
  81. package/src/analyzers/FileUploadAnalyzer.ts +239 -0
  82. package/src/analyzers/IdorAnalyzer.ts +118 -0
  83. package/src/analyzers/InsecureDeserializationAnalyzer.ts +212 -0
  84. package/src/analyzers/MassAssignmentAnalyzer.ts +105 -0
  85. package/src/analyzers/OpenRedirectAnalyzer.ts +149 -0
  86. package/src/analyzers/PrivilegeEscalationAnalyzer.ts +258 -0
  87. package/src/analyzers/RateLimitAnalyzer.ts +195 -0
  88. package/src/analyzers/SecurityHeaderAnalyzer.ts +263 -0
  89. package/src/analyzers/SessionSecurityAnalyzer.ts +342 -0
  90. package/src/analyzers/SqlInjectionAnalyzer.ts +99 -0
  91. package/src/analyzers/XssSurfaceAnalyzer.ts +112 -0
  92. package/src/analyzers/exclusions.ts +87 -0
  93. package/src/analyzers/index.ts +15 -0
  94. package/src/index.ts +226 -0
  95. package/src/parsers/PhpParser.ts +259 -0
  96. package/src/parsers/RouteParser.ts +384 -0
  97. package/src/rules/index.ts +16 -0
  98. package/src/types/index.ts +164 -0
  99. package/tests/EncryptionAnalyzer.test.ts +137 -0
  100. package/tests/PrivilegeEscalationAnalyzer.test.ts +141 -0
  101. package/tests/RateLimitAnalyzer.test.ts +112 -0
  102. package/tests/analyzers.test.ts +678 -0
  103. package/tests/auth-coverage-route-aware.test.ts +294 -0
  104. package/tests/credential-exposure.test.ts +142 -0
  105. package/tests/file-upload.test.ts +141 -0
  106. package/tests/fixtures/app/Http/Controllers/AdminController.php +19 -0
  107. package/tests/fixtures/app/Http/Controllers/PostController.php +49 -0
  108. package/tests/fixtures/app/Http/Controllers/PublicController.php +17 -0
  109. package/tests/fixtures/app/Models/Comment.php +11 -0
  110. package/tests/fixtures/app/Models/OpenModel.php +11 -0
  111. package/tests/fixtures/app/Models/Post.php +14 -0
  112. package/tests/fixtures/app/Models/SafeModel.php +10 -0
  113. package/tests/fixtures/app/Models/User.php +15 -0
  114. package/tests/fixtures/blade/mail.blade.php +8 -0
  115. package/tests/fixtures/blade/safe.blade.php +12 -0
  116. package/tests/fixtures/blade/vulnerable.blade.php +12 -0
  117. package/tests/fixtures/controllers/AdminController.php +19 -0
  118. package/tests/fixtures/controllers/PostController.php +49 -0
  119. package/tests/fixtures/controllers/PublicController.php +17 -0
  120. package/tests/fixtures/deserialization/safe.php +32 -0
  121. package/tests/fixtures/deserialization/unsafe.php +60 -0
  122. package/tests/fixtures/models/Comment.php +11 -0
  123. package/tests/fixtures/models/OpenModel.php +11 -0
  124. package/tests/fixtures/models/Post.php +14 -0
  125. package/tests/fixtures/models/SafeModel.php +10 -0
  126. package/tests/fixtures/models/User.php +15 -0
  127. package/tests/fixtures/redirect/safe.php +38 -0
  128. package/tests/fixtures/redirect/unsafe.php +39 -0
  129. package/tests/fixtures/routes/api.php +9 -0
  130. package/tests/fixtures/routes/web.php +18 -0
  131. package/tests/fixtures/security-headers/app/Http/Middleware/SecurityHeaders.php +24 -0
  132. package/tests/fixtures/security-headers/app/Providers/AppServiceProvider.php +16 -0
  133. package/tests/fixtures/sql/safe_queries.php +7 -0
  134. package/tests/fixtures/sql/vulnerable_queries.php +7 -0
  135. package/tests/new-analyzers.test.ts +373 -0
  136. package/tests/route-parser.test.ts +257 -0
  137. package/tests/scanner.test.ts +82 -0
  138. package/tests/session-security.test.ts +161 -0
  139. package/tests/types.test.ts +29 -0
  140. package/tsconfig.json +9 -0
@@ -0,0 +1,212 @@
1
+ import { Analyzer, Finding, HavocConfig, ParsedFile, Severity } from '../types/index.js';
2
+
3
+ // ─── Constants ────────────────────────────────────────────────────────────────
4
+
5
+ const ANALYZER_NAME = 'InsecureDeserializationAnalyzer';
6
+
7
+ const USER_INPUT_SOURCES = [
8
+ /\$_GET\s*\[/,
9
+ /\$_POST\s*\[/,
10
+ /\$_COOKIE\s*\[/,
11
+ /\$_REQUEST\s*\[/,
12
+ /\$request\s*->\s*input\s*\(/i,
13
+ /\$request\s*->\s*get\s*\(/i,
14
+ /\$request\s*->\s*post\s*\(/i,
15
+ /\$request\s*->\s*cookie\s*\(/i,
16
+ /\$request\s*->\s*query\s*\(/i,
17
+ /\$request\s*->\s*all\s*\(/i,
18
+ /Input\s*::\s*get\s*\(/i,
19
+ /Input\s*::\s*all\s*\(/i,
20
+ ];
21
+
22
+ const PREG_REPLACE_E_PATTERN =
23
+ /preg_replace\s*\(\s*['"][^'"]*\/e[^'"]*['"]/i;
24
+
25
+ const ASSERT_STRING_PATTERN =
26
+ /\bassert\s*\(\s*(?:'[^']*'|"[^"]*"|\$[a-zA-Z_]\w*\s*\.)/;
27
+
28
+ // ─── Finding IDs ──────────────────────────────────────────────────────────────
29
+
30
+ const FINDING_UNSERIALIZE_USER_INPUT = 'HAVOC-DESER-001';
31
+ const FINDING_EVAL = 'HAVOC-DESER-002';
32
+ const FINDING_CREATE_FUNCTION = 'HAVOC-DESER-003';
33
+ const FINDING_UNSERIALIZE_NO_ALLOWED_CLASSES = 'HAVOC-DESER-004';
34
+ const FINDING_PREG_REPLACE_E = 'HAVOC-DESER-005';
35
+ const FINDING_ASSERT_STRING = 'HAVOC-DESER-006';
36
+ const FINDING_EXTRACT_USER_INPUT = 'HAVOC-DESER-007';
37
+
38
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
39
+
40
+ function hasUserInput(line: string): boolean {
41
+ return USER_INPUT_SOURCES.some((re) => re.test(line));
42
+ }
43
+
44
+ function hasAllowedClasses(line: string): boolean {
45
+ return /unserialize\s*\([^)]*allowed_classes/i.test(line);
46
+ }
47
+
48
+ // ─── Analyzer ─────────────────────────────────────────────────────────────────
49
+
50
+ export class InsecureDeserializationAnalyzer implements Analyzer {
51
+ readonly name = ANALYZER_NAME;
52
+ readonly description =
53
+ 'Detects unsafe deserialization and code-execution sinks: unserialize(), eval(), create_function(), preg_replace /e, assert() with string, and extract() on user input';
54
+
55
+ async analyze(files: ParsedFile[], _config: HavocConfig): Promise<Finding[]> {
56
+ const findings: Finding[] = [];
57
+
58
+ for (const file of files) {
59
+ if (!file.path.endsWith('.php') || file.path.endsWith('.blade.php')) continue;
60
+
61
+ const lines = file.content.split('\n');
62
+
63
+ lines.forEach((lineText, idx) => {
64
+ const lineNum = idx + 1;
65
+ const trimmed = lineText.trim();
66
+
67
+ // ── unserialize() ──────────────────────────────────────────────────
68
+ if (/\bunserialize\s*\(/i.test(lineText)) {
69
+ if (hasUserInput(lineText)) {
70
+ findings.push({
71
+ id: FINDING_UNSERIALIZE_USER_INPUT,
72
+ severity: Severity.Critical,
73
+ analyzer: ANALYZER_NAME,
74
+ title: 'unserialize() called with user-controlled input',
75
+ description:
76
+ '`unserialize()` is called with data that appears to originate from user input. ' +
77
+ 'Deserializing untrusted data can lead to remote code execution via PHP object injection.',
78
+ file: file.path,
79
+ line: lineNum,
80
+ recommendation:
81
+ 'Never deserialize user-supplied data. Use a safe data format such as JSON ' +
82
+ '(`json_decode()`). If you must use `unserialize()`, pass ' +
83
+ '`["allowed_classes" => false]` as the second argument and validate input first.',
84
+ cwe: 'CWE-502',
85
+ snippet: trimmed,
86
+ });
87
+ } else if (!hasAllowedClasses(lineText)) {
88
+ findings.push({
89
+ id: FINDING_UNSERIALIZE_NO_ALLOWED_CLASSES,
90
+ severity: Severity.Medium,
91
+ analyzer: ANALYZER_NAME,
92
+ title: 'unserialize() called without allowed_classes restriction',
93
+ description:
94
+ '`unserialize()` is called without the `allowed_classes` option (PHP 7+). ' +
95
+ 'Without this restriction, any class available in the codebase can be instantiated ' +
96
+ 'during deserialization.',
97
+ file: file.path,
98
+ line: lineNum,
99
+ recommendation:
100
+ 'Pass `["allowed_classes" => false]` (or a specific allowlist) as the second argument: ' +
101
+ '`unserialize($data, ["allowed_classes" => false])`.',
102
+ cwe: 'CWE-502',
103
+ snippet: trimmed,
104
+ });
105
+ }
106
+ }
107
+
108
+ // ── eval() ────────────────────────────────────────────────────────
109
+ if (/\beval\s*\(/i.test(lineText)) {
110
+ findings.push({
111
+ id: FINDING_EVAL,
112
+ severity: Severity.High,
113
+ analyzer: ANALYZER_NAME,
114
+ title: 'eval() usage detected',
115
+ description:
116
+ '`eval()` executes arbitrary PHP code. If any portion of the evaluated string ' +
117
+ 'originates from user input, this is a critical remote code execution vulnerability.',
118
+ file: file.path,
119
+ line: lineNum,
120
+ recommendation:
121
+ 'Remove `eval()`. Redesign the feature to avoid dynamic code execution. ' +
122
+ 'If templating is needed, use a sandboxed template engine.',
123
+ cwe: 'CWE-95',
124
+ snippet: trimmed,
125
+ });
126
+ }
127
+
128
+ // ── create_function() ─────────────────────────────────────────────
129
+ if (/\bcreate_function\s*\(/i.test(lineText)) {
130
+ findings.push({
131
+ id: FINDING_CREATE_FUNCTION,
132
+ severity: Severity.High,
133
+ analyzer: ANALYZER_NAME,
134
+ title: 'create_function() usage detected',
135
+ description:
136
+ '`create_function()` is deprecated (PHP 7.2) and removed (PHP 8.0). ' +
137
+ 'It internally uses `eval()` and is equivalent to a code injection sink.',
138
+ file: file.path,
139
+ line: lineNum,
140
+ recommendation:
141
+ 'Replace `create_function()` with an anonymous function (closure): ' +
142
+ '`function($args) { /* body */ }`.',
143
+ cwe: 'CWE-95',
144
+ snippet: trimmed,
145
+ });
146
+ }
147
+
148
+ // ── preg_replace() with /e modifier ──────────────────────────────
149
+ if (PREG_REPLACE_E_PATTERN.test(lineText)) {
150
+ findings.push({
151
+ id: FINDING_PREG_REPLACE_E,
152
+ severity: Severity.High,
153
+ analyzer: ANALYZER_NAME,
154
+ title: 'preg_replace() with /e modifier detected',
155
+ description:
156
+ '`preg_replace()` with the `/e` (eval) modifier evaluates the replacement string ' +
157
+ 'as PHP code. This modifier was deprecated in PHP 5.5 and removed in PHP 7.0.',
158
+ file: file.path,
159
+ line: lineNum,
160
+ recommendation:
161
+ 'Replace with `preg_replace_callback()` which accepts a proper closure.',
162
+ cwe: 'CWE-95',
163
+ snippet: trimmed,
164
+ });
165
+ }
166
+
167
+ // ── assert() with string argument ────────────────────────────────
168
+ if (ASSERT_STRING_PATTERN.test(lineText)) {
169
+ findings.push({
170
+ id: FINDING_ASSERT_STRING,
171
+ severity: Severity.High,
172
+ analyzer: ANALYZER_NAME,
173
+ title: 'assert() called with a string argument',
174
+ description:
175
+ '`assert()` evaluates string arguments as PHP code (similar to `eval()`). ' +
176
+ 'Passing user-controlled strings to `assert()` is a code injection vulnerability.',
177
+ file: file.path,
178
+ line: lineNum,
179
+ recommendation:
180
+ 'Pass a boolean expression directly to `assert()` instead of a string: ' +
181
+ '`assert($value > 0)`. Never pass user-controlled strings.',
182
+ cwe: 'CWE-95',
183
+ snippet: trimmed,
184
+ });
185
+ }
186
+
187
+ // ── extract() on user input ───────────────────────────────────────
188
+ if (/\bextract\s*\(/i.test(lineText) && hasUserInput(lineText)) {
189
+ findings.push({
190
+ id: FINDING_EXTRACT_USER_INPUT,
191
+ severity: Severity.High,
192
+ analyzer: ANALYZER_NAME,
193
+ title: 'extract() called with user-controlled input',
194
+ description:
195
+ '`extract()` imports array keys as variable names. Calling it with user-supplied ' +
196
+ 'data allows an attacker to overwrite arbitrary variables in the current scope, ' +
197
+ 'potentially leading to authentication bypass or code execution.',
198
+ file: file.path,
199
+ line: lineNum,
200
+ recommendation:
201
+ 'Never call `extract()` on user-supplied data. Access individual keys explicitly: ' +
202
+ '`$value = $data["key"] ?? null`.',
203
+ cwe: 'CWE-621',
204
+ snippet: trimmed,
205
+ });
206
+ }
207
+ });
208
+ }
209
+
210
+ return findings;
211
+ }
212
+ }
@@ -0,0 +1,105 @@
1
+ import { Analyzer, Finding, HavocConfig, ParsedFile, Severity } from '../types/index.js';
2
+ import type { ParsedPhpFile } from '../parsers/PhpParser.js';
3
+
4
+ const SENSITIVE_FILLABLE_FIELDS = new Set([
5
+ 'password', 'password_hash', 'password_confirmation', 'role', 'roles',
6
+ 'is_admin', 'admin', 'is_superuser', 'email_verified_at', 'remember_token',
7
+ 'api_token', 'api_key', 'stripe_id', 'two_factor_secret', 'two_factor_recovery_codes',
8
+ ]);
9
+
10
+ const FINDING_NO_GUARD = 'HAVOC-MA-001';
11
+ const FINDING_OPEN_GUARD = 'HAVOC-MA-002';
12
+ const FINDING_SENSITIVE = 'HAVOC-MA-003';
13
+ const ANALYZER_NAME = 'MassAssignmentAnalyzer';
14
+
15
+ function isModelFile(path: string): boolean {
16
+ return path.includes('app/Models') || path.includes('app\\Models');
17
+ }
18
+
19
+ export class MassAssignmentAnalyzer implements Analyzer {
20
+ readonly name = ANALYZER_NAME;
21
+ readonly description = 'Detects Eloquent models vulnerable to mass assignment attacks';
22
+
23
+ async analyze(files: ParsedFile[], _config: HavocConfig): Promise<Finding[]> {
24
+ const findings: Finding[] = [];
25
+
26
+ for (const file of files) {
27
+ if (!isModelFile(file.path)) continue;
28
+
29
+ const phpFile = file as ParsedPhpFile;
30
+ if (!phpFile.classes) continue;
31
+
32
+ for (const cls of phpFile.classes) {
33
+ if (cls.isAbstract || cls.isTrait) continue;
34
+
35
+ const fillableProp = cls.properties.find((p) => p.name === 'fillable');
36
+ const guardedProp = cls.properties.find((p) => p.name === 'guarded');
37
+ const hasFillable = fillableProp !== undefined;
38
+ const hasGuarded = guardedProp !== undefined;
39
+
40
+ if (!hasFillable && !hasGuarded) {
41
+ findings.push({
42
+ id: FINDING_NO_GUARD,
43
+ severity: Severity.Medium,
44
+ analyzer: ANALYZER_NAME,
45
+ title: `Model \`${cls.name}\` has no mass assignment protection`,
46
+ description:
47
+ `The Eloquent model \`${cls.name}\` defines neither \`$fillable\` nor \`$guarded\`. ` +
48
+ `Without these, all attributes may be mass-assignable.`,
49
+ file: file.path,
50
+ line: cls.line,
51
+ recommendation:
52
+ 'Define `$fillable` with an explicit list of safe attributes, or define `$guarded = [\'id\']`.',
53
+ cwe: 'CWE-915',
54
+ });
55
+ continue;
56
+ }
57
+
58
+ if (hasGuarded) {
59
+ const initText = guardedProp?.initializer ?? '';
60
+ const isOpenGuard = initText.trim() === '[]';
61
+ if (isOpenGuard) {
62
+ findings.push({
63
+ id: FINDING_OPEN_GUARD,
64
+ severity: Severity.High,
65
+ analyzer: ANALYZER_NAME,
66
+ title: `Model \`${cls.name}\` uses \`$guarded = []\` (effectively unguarded)`,
67
+ description:
68
+ `\`${cls.name}\` sets \`$guarded = []\`, which disables all mass assignment protection.`,
69
+ file: file.path,
70
+ line: guardedProp?.line ?? cls.line,
71
+ recommendation: 'Switch to `$fillable` with an explicit allowlist.',
72
+ cwe: 'CWE-915',
73
+ snippet: 'protected $guarded = [];',
74
+ });
75
+ }
76
+ }
77
+
78
+ if (hasFillable) {
79
+ const initText = fillableProp?.initializer ?? '';
80
+ for (const sensitive of SENSITIVE_FILLABLE_FIELDS) {
81
+ if (initText.includes('"' + sensitive + '"') || initText.includes("'" + sensitive + "'")) {
82
+ findings.push({
83
+ id: FINDING_SENSITIVE,
84
+ severity: Severity.Medium,
85
+ analyzer: ANALYZER_NAME,
86
+ title: `Sensitive field \`${sensitive}\` is mass-assignable in \`${cls.name}\``,
87
+ description:
88
+ `The field \`${sensitive}\` is listed in \`$fillable\` on model \`${cls.name}\`. ` +
89
+ `Allowing mass assignment of this field could allow privilege escalation.`,
90
+ file: file.path,
91
+ line: fillableProp?.line ?? cls.line,
92
+ recommendation:
93
+ `Remove \`${sensitive}\` from \`$fillable\` and set it via dedicated methods.`,
94
+ cwe: 'CWE-915',
95
+ snippet: "protected $fillable = [..., '" + sensitive + "', ...];",
96
+ });
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ return findings;
104
+ }
105
+ }
@@ -0,0 +1,149 @@
1
+ import { Analyzer, Finding, HavocConfig, ParsedFile, Severity } from '../types/index.js';
2
+
3
+ // ─── Constants ────────────────────────────────────────────────────────────────
4
+
5
+ const ANALYZER_NAME = 'OpenRedirectAnalyzer';
6
+
7
+ const FINDING_DIRECT = 'HAVOC-REDIRECT-001';
8
+ const FINDING_INDIRECT = 'HAVOC-REDIRECT-002';
9
+
10
+ /**
11
+ * Common open-redirect parameter names that attackers target.
12
+ */
13
+ const REDIRECT_PARAM_NAMES = [
14
+ 'url', 'redirect', 'next', 'return', 'return_to',
15
+ 'callback', 'goto', 'dest', 'destination', 'forward',
16
+ 'target', 'redir', 'redirect_url', 'redirect_uri',
17
+ ];
18
+
19
+ /**
20
+ * Patterns that indicate the destination is user-controlled (inline in the call).
21
+ */
22
+ const DIRECT_USER_INPUT_PATTERNS = [
23
+ // redirect($request->input('url'))
24
+ /\bredirect\s*\(\s*\$request\s*->\s*(?:input|get|query|post|cookie)\s*\(/i,
25
+ // Redirect::to($request->input('url'))
26
+ /Redirect\s*::\s*to\s*\(\s*\$request\s*->\s*(?:input|get|query|post|cookie)\s*\(/i,
27
+ // redirect($_GET['url'])
28
+ /\bredirect\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE)\s*\[/i,
29
+ // Redirect::to($_GET['url'])
30
+ /Redirect\s*::\s*to\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE)\s*\[/i,
31
+ // header('Location: ' . $userVar) — where the var comes right after concat
32
+ /header\s*\(\s*['"]Location:\s*['"]\s*\.\s*\$(?:_GET|_POST|_REQUEST|_COOKIE)\s*\[/i,
33
+ ];
34
+
35
+ /**
36
+ * Patterns that look like redirect to a variable (indirect).
37
+ */
38
+ const INDIRECT_REDIRECT_PATTERNS = [
39
+ // redirect($url) / redirect($destination) etc.
40
+ new RegExp(
41
+ `\\bredirect\\s*\\(\\s*\\$(?:${REDIRECT_PARAM_NAMES.join('|')})\\b`,
42
+ 'i',
43
+ ),
44
+ // Redirect::to($url)
45
+ new RegExp(
46
+ `Redirect\\s*::\\s*to\\s*\\(\\s*\\$(?:${REDIRECT_PARAM_NAMES.join('|')})\\b`,
47
+ 'i',
48
+ ),
49
+ // header('Location: ' . $url)
50
+ new RegExp(
51
+ `header\\s*\\(\\s*['"]Location[^'"]*['"]\\s*\\.\\s*\\$(?:${REDIRECT_PARAM_NAMES.join('|')})\\b`,
52
+ 'i',
53
+ ),
54
+ ];
55
+
56
+ /**
57
+ * Safe redirect helpers — skip these to avoid false positives.
58
+ */
59
+ const SAFE_PATTERNS = [
60
+ /\bredirect\s*\(\s*\)\s*->\s*intended\s*\(/i,
61
+ /\bback\s*\(\s*\)/i,
62
+ /Redirect\s*::\s*intended\s*\(/i,
63
+ /Redirect\s*::\s*back\s*\(/i,
64
+ // redirect(route(...)) or redirect(url(...)) — trusted helpers
65
+ /\bredirect\s*\(\s*(?:route|url|action|asset)\s*\(/i,
66
+ /Redirect\s*::\s*to\s*\(\s*(?:route|url|action|asset)\s*\(/i,
67
+ // redirect('/explicit-path') — single-quoted string is always safe
68
+ // redirect("/path") — double-quoted without $ interpolation is safe
69
+ /\bredirect\s*\(\s*'/i,
70
+ /Redirect\s*::\s*to\s*\(\s*'/i,
71
+ /\bredirect\s*\(\s*"[^"$]*"\s*\)/i,
72
+ /Redirect\s*::\s*to\s*\(\s*"[^"$]*"\s*\)/i,
73
+ ];
74
+
75
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
76
+
77
+ function isSafe(line: string): boolean {
78
+ return SAFE_PATTERNS.some((re) => re.test(line));
79
+ }
80
+
81
+ // ─── Analyzer ─────────────────────────────────────────────────────────────────
82
+
83
+ export class OpenRedirectAnalyzer implements Analyzer {
84
+ readonly name = ANALYZER_NAME;
85
+ readonly description =
86
+ 'Detects open redirect vulnerabilities where user-controlled input is used as a redirect destination without validation';
87
+
88
+ async analyze(files: ParsedFile[], _config: HavocConfig): Promise<Finding[]> {
89
+ const findings: Finding[] = [];
90
+
91
+ for (const file of files) {
92
+ if (!file.path.endsWith('.php') || file.path.endsWith('.blade.php')) continue;
93
+
94
+ const lines = file.content.split('\n');
95
+
96
+ lines.forEach((lineText, idx) => {
97
+ const lineNum = idx + 1;
98
+ const trimmed = lineText.trim();
99
+
100
+ // Skip safe patterns first
101
+ if (isSafe(lineText)) return;
102
+
103
+ // ── Direct user input in redirect ────────────────────────────────
104
+ if (DIRECT_USER_INPUT_PATTERNS.some((re) => re.test(lineText))) {
105
+ findings.push({
106
+ id: FINDING_DIRECT,
107
+ severity: Severity.High,
108
+ analyzer: ANALYZER_NAME,
109
+ title: 'Open redirect: user-controlled redirect destination',
110
+ description:
111
+ 'A redirect is performed using a value that appears to come directly from user input. ' +
112
+ 'An attacker can craft a URL that redirects victims to a malicious site (phishing, credential harvesting).',
113
+ file: file.path,
114
+ line: lineNum,
115
+ recommendation:
116
+ 'Validate redirect destinations against an allowlist of trusted URLs or paths. ' +
117
+ 'Use `redirect()->intended(\'/\')` for post-login redirects. ' +
118
+ 'Never trust `url`, `redirect`, `next`, or similar parameters without validation.',
119
+ cwe: 'CWE-601',
120
+ snippet: trimmed,
121
+ });
122
+ return;
123
+ }
124
+
125
+ // ── Indirect redirect via named variable ──────────────────────────
126
+ if (INDIRECT_REDIRECT_PATTERNS.some((re) => re.test(lineText))) {
127
+ findings.push({
128
+ id: FINDING_INDIRECT,
129
+ severity: Severity.Medium,
130
+ analyzer: ANALYZER_NAME,
131
+ title: 'Open redirect: redirect destination via variable — verify validation',
132
+ description:
133
+ 'A redirect is performed using a variable with a common redirect-parameter name. ' +
134
+ 'If this variable is populated from user input without validation, it is an open redirect vulnerability.',
135
+ file: file.path,
136
+ line: lineNum,
137
+ recommendation:
138
+ 'Ensure the redirect destination is validated against an allowlist of trusted domains/paths ' +
139
+ 'before use. Consider using `redirect()->intended(\'/dashboard\')` instead.',
140
+ cwe: 'CWE-601',
141
+ snippet: trimmed,
142
+ });
143
+ }
144
+ });
145
+ }
146
+
147
+ return findings;
148
+ }
149
+ }