@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.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +22 -0
- package/dist/analyzers/AuthorizationCoverageAnalyzer.d.ts +7 -0
- package/dist/analyzers/AuthorizationCoverageAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/AuthorizationCoverageAnalyzer.js +100 -0
- package/dist/analyzers/AuthorizationCoverageAnalyzer.js.map +1 -0
- package/dist/analyzers/CredentialExposureAnalyzer.d.ts +11 -0
- package/dist/analyzers/CredentialExposureAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/CredentialExposureAnalyzer.js +262 -0
- package/dist/analyzers/CredentialExposureAnalyzer.js.map +1 -0
- package/dist/analyzers/DependencyAuditAnalyzer.d.ts +28 -0
- package/dist/analyzers/DependencyAuditAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/DependencyAuditAnalyzer.js +107 -0
- package/dist/analyzers/DependencyAuditAnalyzer.js.map +1 -0
- package/dist/analyzers/EncryptionAnalyzer.d.ts +7 -0
- package/dist/analyzers/EncryptionAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/EncryptionAnalyzer.js +170 -0
- package/dist/analyzers/EncryptionAnalyzer.js.map +1 -0
- package/dist/analyzers/FileUploadAnalyzer.d.ts +8 -0
- package/dist/analyzers/FileUploadAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/FileUploadAnalyzer.js +193 -0
- package/dist/analyzers/FileUploadAnalyzer.js.map +1 -0
- package/dist/analyzers/IdorAnalyzer.d.ts +7 -0
- package/dist/analyzers/IdorAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/IdorAnalyzer.js +91 -0
- package/dist/analyzers/IdorAnalyzer.js.map +1 -0
- package/dist/analyzers/MassAssignmentAnalyzer.d.ts +7 -0
- package/dist/analyzers/MassAssignmentAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/MassAssignmentAnalyzer.js +90 -0
- package/dist/analyzers/MassAssignmentAnalyzer.js.map +1 -0
- package/dist/analyzers/PrivilegeEscalationAnalyzer.d.ts +7 -0
- package/dist/analyzers/PrivilegeEscalationAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/PrivilegeEscalationAnalyzer.js +217 -0
- package/dist/analyzers/PrivilegeEscalationAnalyzer.js.map +1 -0
- package/dist/analyzers/RateLimitAnalyzer.d.ts +7 -0
- package/dist/analyzers/RateLimitAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/RateLimitAnalyzer.js +151 -0
- package/dist/analyzers/RateLimitAnalyzer.js.map +1 -0
- package/dist/analyzers/SessionSecurityAnalyzer.d.ts +10 -0
- package/dist/analyzers/SessionSecurityAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/SessionSecurityAnalyzer.js +295 -0
- package/dist/analyzers/SessionSecurityAnalyzer.js.map +1 -0
- package/dist/analyzers/SqlInjectionAnalyzer.d.ts +7 -0
- package/dist/analyzers/SqlInjectionAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/SqlInjectionAnalyzer.js +77 -0
- package/dist/analyzers/SqlInjectionAnalyzer.js.map +1 -0
- package/dist/analyzers/XssSurfaceAnalyzer.d.ts +7 -0
- package/dist/analyzers/XssSurfaceAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/XssSurfaceAnalyzer.js +100 -0
- package/dist/analyzers/XssSurfaceAnalyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +13 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +13 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +139 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/PhpParser.d.ts +56 -0
- package/dist/parsers/PhpParser.d.ts.map +1 -0
- package/dist/parsers/PhpParser.js +193 -0
- package/dist/parsers/PhpParser.js.map +1 -0
- package/dist/parsers/RouteParser.d.ts +87 -0
- package/dist/parsers/RouteParser.d.ts.map +1 -0
- package/dist/parsers/RouteParser.js +327 -0
- package/dist/parsers/RouteParser.js.map +1 -0
- package/dist/rules/index.d.ts +14 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +9 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/types/index.d.ts +137 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +30 -0
- package/package.json.bak +27 -0
- package/src/analyzers/AuthorizationCoverageAnalyzer.ts +213 -0
- package/src/analyzers/CredentialExposureAnalyzer.ts +312 -0
- package/src/analyzers/DependencyAuditAnalyzer.ts +135 -0
- package/src/analyzers/EncryptionAnalyzer.ts +195 -0
- package/src/analyzers/FileUploadAnalyzer.ts +239 -0
- package/src/analyzers/IdorAnalyzer.ts +118 -0
- package/src/analyzers/InsecureDeserializationAnalyzer.ts +212 -0
- package/src/analyzers/MassAssignmentAnalyzer.ts +105 -0
- package/src/analyzers/OpenRedirectAnalyzer.ts +149 -0
- package/src/analyzers/PrivilegeEscalationAnalyzer.ts +258 -0
- package/src/analyzers/RateLimitAnalyzer.ts +195 -0
- package/src/analyzers/SecurityHeaderAnalyzer.ts +263 -0
- package/src/analyzers/SessionSecurityAnalyzer.ts +342 -0
- package/src/analyzers/SqlInjectionAnalyzer.ts +99 -0
- package/src/analyzers/XssSurfaceAnalyzer.ts +112 -0
- package/src/analyzers/exclusions.ts +87 -0
- package/src/analyzers/index.ts +15 -0
- package/src/index.ts +226 -0
- package/src/parsers/PhpParser.ts +259 -0
- package/src/parsers/RouteParser.ts +384 -0
- package/src/rules/index.ts +16 -0
- package/src/types/index.ts +164 -0
- package/tests/EncryptionAnalyzer.test.ts +137 -0
- package/tests/PrivilegeEscalationAnalyzer.test.ts +141 -0
- package/tests/RateLimitAnalyzer.test.ts +112 -0
- package/tests/analyzers.test.ts +678 -0
- package/tests/auth-coverage-route-aware.test.ts +294 -0
- package/tests/credential-exposure.test.ts +142 -0
- package/tests/file-upload.test.ts +141 -0
- package/tests/fixtures/app/Http/Controllers/AdminController.php +19 -0
- package/tests/fixtures/app/Http/Controllers/PostController.php +49 -0
- package/tests/fixtures/app/Http/Controllers/PublicController.php +17 -0
- package/tests/fixtures/app/Models/Comment.php +11 -0
- package/tests/fixtures/app/Models/OpenModel.php +11 -0
- package/tests/fixtures/app/Models/Post.php +14 -0
- package/tests/fixtures/app/Models/SafeModel.php +10 -0
- package/tests/fixtures/app/Models/User.php +15 -0
- package/tests/fixtures/blade/mail.blade.php +8 -0
- package/tests/fixtures/blade/safe.blade.php +12 -0
- package/tests/fixtures/blade/vulnerable.blade.php +12 -0
- package/tests/fixtures/controllers/AdminController.php +19 -0
- package/tests/fixtures/controllers/PostController.php +49 -0
- package/tests/fixtures/controllers/PublicController.php +17 -0
- package/tests/fixtures/deserialization/safe.php +32 -0
- package/tests/fixtures/deserialization/unsafe.php +60 -0
- package/tests/fixtures/models/Comment.php +11 -0
- package/tests/fixtures/models/OpenModel.php +11 -0
- package/tests/fixtures/models/Post.php +14 -0
- package/tests/fixtures/models/SafeModel.php +10 -0
- package/tests/fixtures/models/User.php +15 -0
- package/tests/fixtures/redirect/safe.php +38 -0
- package/tests/fixtures/redirect/unsafe.php +39 -0
- package/tests/fixtures/routes/api.php +9 -0
- package/tests/fixtures/routes/web.php +18 -0
- package/tests/fixtures/security-headers/app/Http/Middleware/SecurityHeaders.php +24 -0
- package/tests/fixtures/security-headers/app/Providers/AppServiceProvider.php +16 -0
- package/tests/fixtures/sql/safe_queries.php +7 -0
- package/tests/fixtures/sql/vulnerable_queries.php +7 -0
- package/tests/new-analyzers.test.ts +373 -0
- package/tests/route-parser.test.ts +257 -0
- package/tests/scanner.test.ts +82 -0
- package/tests/session-security.test.ts +161 -0
- package/tests/types.test.ts +29 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Severity } from '../types/index.js';
|
|
2
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
3
|
+
const FINDING_UNENCRYPTED_SENSITIVE = 'HAVOC-ENC-001';
|
|
4
|
+
const FINDING_MISSING_CAST = 'HAVOC-ENC-002';
|
|
5
|
+
const FINDING_APP_KEY_ROTATION = 'HAVOC-ENC-003';
|
|
6
|
+
const ANALYZER_NAME = 'EncryptionAnalyzer';
|
|
7
|
+
/**
|
|
8
|
+
* Field names considered sensitive PII / secrets that should be encrypted at rest.
|
|
9
|
+
*/
|
|
10
|
+
const SENSITIVE_FIELDS = new Set([
|
|
11
|
+
'ssn',
|
|
12
|
+
'social_security_number',
|
|
13
|
+
'tax_id',
|
|
14
|
+
'ein',
|
|
15
|
+
'credit_card',
|
|
16
|
+
'credit_card_number',
|
|
17
|
+
'card_number',
|
|
18
|
+
'bank_account',
|
|
19
|
+
'bank_account_number',
|
|
20
|
+
'routing_number',
|
|
21
|
+
'api_key',
|
|
22
|
+
'api_secret',
|
|
23
|
+
'secret',
|
|
24
|
+
'secret_key',
|
|
25
|
+
'private_key',
|
|
26
|
+
'access_token',
|
|
27
|
+
'refresh_token',
|
|
28
|
+
'oauth_token',
|
|
29
|
+
'personal_access_token',
|
|
30
|
+
'webhook_secret',
|
|
31
|
+
'encryption_key',
|
|
32
|
+
'date_of_birth',
|
|
33
|
+
'dob',
|
|
34
|
+
'passport_number',
|
|
35
|
+
'drivers_license',
|
|
36
|
+
'medical_record',
|
|
37
|
+
]);
|
|
38
|
+
/** Encrypted cast values that satisfy the requirement. */
|
|
39
|
+
const ENCRYPTED_CASTS = new Set(['encrypted', 'encrypted:array', 'encrypted:object', 'encrypted:collection', 'encrypted:json']);
|
|
40
|
+
function isModelFile(path) {
|
|
41
|
+
return path.includes('app/Models') || path.includes('app\\Models');
|
|
42
|
+
}
|
|
43
|
+
function extractFillableFields(initText) {
|
|
44
|
+
const matches = initText.match(/['"]([^'"]+)['"]/g) ?? [];
|
|
45
|
+
return matches.map((m) => m.replace(/['"]/g, ''));
|
|
46
|
+
}
|
|
47
|
+
function extractCastValues(castsText) {
|
|
48
|
+
const map = new Map();
|
|
49
|
+
// Match 'field' => 'cast_type' pairs
|
|
50
|
+
const pairRegex = /['"]([^'"]+)['"]\s*=>\s*['"]([^'"]+)['"]/g;
|
|
51
|
+
let m;
|
|
52
|
+
while ((m = pairRegex.exec(castsText)) !== null) {
|
|
53
|
+
map.set(m[1], m[2]);
|
|
54
|
+
}
|
|
55
|
+
return map;
|
|
56
|
+
}
|
|
57
|
+
export class EncryptionAnalyzer {
|
|
58
|
+
name = ANALYZER_NAME;
|
|
59
|
+
description = 'Detects sensitive model attributes missing encryption casts and other encryption issues';
|
|
60
|
+
async analyze(files, _config) {
|
|
61
|
+
const findings = [];
|
|
62
|
+
let hasAppFiles = false;
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
// Check for APP_KEY usage hints (only once)
|
|
65
|
+
if (!hasAppFiles && (file.path === '.env' || file.path === 'config/app.php')) {
|
|
66
|
+
hasAppFiles = true;
|
|
67
|
+
}
|
|
68
|
+
if (!isModelFile(file.path))
|
|
69
|
+
continue;
|
|
70
|
+
const phpFile = file;
|
|
71
|
+
if (!phpFile.classes)
|
|
72
|
+
continue;
|
|
73
|
+
for (const cls of phpFile.classes) {
|
|
74
|
+
if (cls.isAbstract || cls.isTrait)
|
|
75
|
+
continue;
|
|
76
|
+
const fillableProp = cls.properties.find((p) => p.name === 'fillable');
|
|
77
|
+
const castsProp = cls.properties.find((p) => p.name === 'casts');
|
|
78
|
+
const fillableFields = fillableProp ? extractFillableFields(fillableProp.initializer ?? '') : [];
|
|
79
|
+
const castsMap = castsProp ? extractCastValues(castsProp.initializer ?? '') : new Map();
|
|
80
|
+
for (const field of fillableFields) {
|
|
81
|
+
if (!SENSITIVE_FIELDS.has(field))
|
|
82
|
+
continue;
|
|
83
|
+
const castValue = castsMap.get(field);
|
|
84
|
+
const isEncrypted = castValue !== undefined && ENCRYPTED_CASTS.has(castValue);
|
|
85
|
+
if (!isEncrypted) {
|
|
86
|
+
if (castsProp && castsMap.has(field)) {
|
|
87
|
+
// Has a cast but not encrypted
|
|
88
|
+
findings.push({
|
|
89
|
+
id: FINDING_MISSING_CAST,
|
|
90
|
+
severity: Severity.Medium,
|
|
91
|
+
analyzer: ANALYZER_NAME,
|
|
92
|
+
title: `Sensitive field \`${field}\` in \`${cls.name}\` is not cast as encrypted`,
|
|
93
|
+
description: `The field \`${field}\` in model \`${cls.name}\` is cast as \`${castValue}\` ` +
|
|
94
|
+
`but not encrypted. Sensitive PII/secret fields should use an encrypted cast ` +
|
|
95
|
+
`to protect data at rest.`,
|
|
96
|
+
file: file.path,
|
|
97
|
+
line: castsProp?.line ?? fillableProp?.line ?? cls.line,
|
|
98
|
+
recommendation: `Change the cast to \`'${field}' => 'encrypted'\` (or \`encrypted:array\` / ` +
|
|
99
|
+
`\`encrypted:object\` for structured data). Requires \`APP_KEY\` to be set.`,
|
|
100
|
+
cwe: 'CWE-312',
|
|
101
|
+
snippet: `protected $casts = ['${field}' => '${castValue}'];`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// No encrypted cast at all — HIGH severity
|
|
106
|
+
findings.push({
|
|
107
|
+
id: FINDING_UNENCRYPTED_SENSITIVE,
|
|
108
|
+
severity: Severity.High,
|
|
109
|
+
analyzer: ANALYZER_NAME,
|
|
110
|
+
title: `Sensitive field \`${field}\` in \`${cls.name}\` is stored unencrypted`,
|
|
111
|
+
description: `The field \`${field}\` is listed in \`$fillable\` on \`${cls.name}\` but has ` +
|
|
112
|
+
`no encrypted cast in \`$casts\`. This means sensitive data is stored in plain ` +
|
|
113
|
+
`text in the database, exposing it if the database is compromised.`,
|
|
114
|
+
file: file.path,
|
|
115
|
+
line: fillableProp?.line ?? cls.line,
|
|
116
|
+
recommendation: `Add \`'${field}' => 'encrypted'\` to the model's \`$casts\` array. ` +
|
|
117
|
+
`Laravel will automatically encrypt/decrypt using \`APP_KEY\`.`,
|
|
118
|
+
cwe: 'CWE-312',
|
|
119
|
+
snippet: `protected $casts = ['${field}' => 'encrypted'];`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Check for Crypt:: usage in the file content (hardcoded key risk)
|
|
125
|
+
const cryptUsageRegex = /Crypt::(encrypt|decrypt)\s*\(/g;
|
|
126
|
+
if (cryptUsageRegex.test(file.content)) {
|
|
127
|
+
// Look for string literals passed directly that might be hardcoded keys
|
|
128
|
+
const hardcodedKeyRegex = /Crypt::(encrypt|decrypt)\s*\(\s*\$[a-zA-Z_]+\s*,\s*['"][^'"]{8,}['"]/;
|
|
129
|
+
if (hardcodedKeyRegex.test(file.content)) {
|
|
130
|
+
findings.push({
|
|
131
|
+
id: FINDING_UNENCRYPTED_SENSITIVE,
|
|
132
|
+
severity: Severity.High,
|
|
133
|
+
analyzer: ANALYZER_NAME,
|
|
134
|
+
title: `Possible hardcoded key in \`Crypt::\` call in \`${cls.name}\``,
|
|
135
|
+
description: `A \`Crypt::encrypt()\` or \`Crypt::decrypt()\` call in \`${cls.name}\` appears ` +
|
|
136
|
+
`to pass a hardcoded string as a key parameter. Laravel's \`Crypt\` facade uses ` +
|
|
137
|
+
`\`APP_KEY\` automatically — passing extra string arguments is unusual and may ` +
|
|
138
|
+
`indicate a misuse or hardcoded secret.`,
|
|
139
|
+
file: file.path,
|
|
140
|
+
line: cls.line,
|
|
141
|
+
recommendation: `Use \`Crypt::encrypt($data)\` without a second argument. Never hardcode ` +
|
|
142
|
+
`encryption keys — use \`APP_KEY\` via the config system.`,
|
|
143
|
+
cwe: 'CWE-321',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// APP_KEY rotation info finding (once per scan if app files present)
|
|
150
|
+
if (hasAppFiles) {
|
|
151
|
+
findings.push({
|
|
152
|
+
id: FINDING_APP_KEY_ROTATION,
|
|
153
|
+
severity: Severity.Info,
|
|
154
|
+
analyzer: ANALYZER_NAME,
|
|
155
|
+
title: 'Ensure regular APP_KEY rotation is planned',
|
|
156
|
+
description: `Laravel's \`APP_KEY\` is used for all encryption and signed URLs. A compromised ` +
|
|
157
|
+
`\`APP_KEY\` exposes all encrypted model attributes. Ensure the key is rotated ` +
|
|
158
|
+
`periodically and after any potential compromise, using \`php artisan key:rotate\` ` +
|
|
159
|
+
`(Laravel 11+) to re-encrypt data gracefully.`,
|
|
160
|
+
file: 'config/app.php',
|
|
161
|
+
line: 1,
|
|
162
|
+
recommendation: 'Rotate APP_KEY using `php artisan key:rotate` (requires `APP_PREVIOUS_KEYS` for ' +
|
|
163
|
+
'graceful re-encryption). Store keys in a secrets manager, never in version control.',
|
|
164
|
+
cwe: 'CWE-320',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return findings;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=EncryptionAnalyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EncryptionAnalyzer.js","sourceRoot":"","sources":["../../src/analyzers/EncryptionAnalyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Analyzer, Finding, HavocConfig, ParsedFile } from '../types/index.js';
|
|
2
|
+
export declare class FileUploadAnalyzer implements Analyzer {
|
|
3
|
+
readonly name = "FileUploadAnalyzer";
|
|
4
|
+
readonly description = "Detects insecure file upload patterns including missing validation and dangerous storage configurations";
|
|
5
|
+
analyze(files: ParsedFile[], _config: HavocConfig): Promise<Finding[]>;
|
|
6
|
+
private checkPathFromInput;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=FileUploadAnalyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileUploadAnalyzer.d.ts","sourceRoot":"","sources":["../../src/analyzers/FileUploadAnalyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAY,MAAM,mBAAmB,CAAC;AAoGzF,qBAAa,kBAAmB,YAAW,QAAQ;IACjD,QAAQ,CAAC,IAAI,wBAAiB;IAC9B,QAAQ,CAAC,WAAW,6GAA6G;IAE3H,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAuG5E,OAAO,CAAC,kBAAkB;CA8B3B"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Severity } from '../types/index.js';
|
|
2
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
3
|
+
const ANALYZER_NAME = 'FileUploadAnalyzer';
|
|
4
|
+
const FINDING_NO_TYPE_VALIDATION = 'HAVOC-UPLOAD-001';
|
|
5
|
+
const FINDING_NO_SIZE_VALIDATION = 'HAVOC-UPLOAD-002';
|
|
6
|
+
const FINDING_PUBLIC_DISK = 'HAVOC-UPLOAD-003';
|
|
7
|
+
const FINDING_EXECUTABLE_EXTENSION = 'HAVOC-UPLOAD-004';
|
|
8
|
+
const FINDING_PATH_FROM_INPUT = 'HAVOC-UPLOAD-005';
|
|
9
|
+
// Executable extensions that should never be allowed in uploads
|
|
10
|
+
const EXECUTABLE_EXTENSIONS = ['.php', '.phtml', '.php3', '.php4', '.php5', '.php7', '.phar', '.blade.php'];
|
|
11
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
12
|
+
function isPhpFile(path) {
|
|
13
|
+
return path.endsWith('.php') && !path.endsWith('.blade.php');
|
|
14
|
+
}
|
|
15
|
+
function findLineNumber(content, matchIndex) {
|
|
16
|
+
return content.slice(0, matchIndex).split('\n').length;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Find method blocks that handle file uploads.
|
|
20
|
+
* Deduplicates blocks by startIndex to avoid duplicate findings when a method
|
|
21
|
+
* contains multiple upload-related tokens.
|
|
22
|
+
*/
|
|
23
|
+
function findUploadBlocks(content) {
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
const blocks = [];
|
|
26
|
+
const uploadPattern = /\$request->(file|hasFile)\s*\(|->store(As)?\s*\(/g;
|
|
27
|
+
let match;
|
|
28
|
+
while ((match = uploadPattern.exec(content)) !== null) {
|
|
29
|
+
const before = content.slice(0, match.index);
|
|
30
|
+
const functionStart = before.lastIndexOf('function ');
|
|
31
|
+
if (functionStart === -1)
|
|
32
|
+
continue;
|
|
33
|
+
if (seen.has(functionStart))
|
|
34
|
+
continue;
|
|
35
|
+
seen.add(functionStart);
|
|
36
|
+
// Find end of method body (brace-balanced)
|
|
37
|
+
let depth = 0;
|
|
38
|
+
let end = functionStart;
|
|
39
|
+
for (let i = functionStart; i < content.length; i++) {
|
|
40
|
+
if (content[i] === '{')
|
|
41
|
+
depth++;
|
|
42
|
+
else if (content[i] === '}') {
|
|
43
|
+
depth--;
|
|
44
|
+
if (depth === 0) {
|
|
45
|
+
end = i;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const methodContent = content.slice(functionStart, end + 1);
|
|
51
|
+
blocks.push({
|
|
52
|
+
startIndex: functionStart,
|
|
53
|
+
endIndex: end,
|
|
54
|
+
snippet: methodContent,
|
|
55
|
+
line: findLineNumber(content, functionStart),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return blocks;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if a method block has file type validation rules.
|
|
62
|
+
*/
|
|
63
|
+
function hasTypeValidation(methodBlock) {
|
|
64
|
+
return /mimes\s*:|mimetypes\s*:|extensions\s*:/i.test(methodBlock);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check if a method block has file size validation.
|
|
68
|
+
*/
|
|
69
|
+
function hasSizeValidation(methodBlock) {
|
|
70
|
+
return /\bmax\s*:/i.test(methodBlock);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extract the validation rules string if present in a method block.
|
|
74
|
+
*/
|
|
75
|
+
function extractValidationRules(methodBlock) {
|
|
76
|
+
const rulesMatch = /(?:validate|rules)\s*\([^)]*\[([^\]]+)\]/s.exec(methodBlock);
|
|
77
|
+
return rulesMatch ? rulesMatch[1] : null;
|
|
78
|
+
}
|
|
79
|
+
// ─── Analyzer ─────────────────────────────────────────────────────────────────
|
|
80
|
+
export class FileUploadAnalyzer {
|
|
81
|
+
name = ANALYZER_NAME;
|
|
82
|
+
description = 'Detects insecure file upload patterns including missing validation and dangerous storage configurations';
|
|
83
|
+
async analyze(files, _config) {
|
|
84
|
+
const findings = [];
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
if (!isPhpFile(file.path))
|
|
87
|
+
continue;
|
|
88
|
+
const content = file.content;
|
|
89
|
+
const uploadBlocks = findUploadBlocks(content);
|
|
90
|
+
for (const block of uploadBlocks) {
|
|
91
|
+
const { snippet, line } = block;
|
|
92
|
+
// 1. Missing file type validation
|
|
93
|
+
if (!hasTypeValidation(snippet)) {
|
|
94
|
+
findings.push({
|
|
95
|
+
id: FINDING_NO_TYPE_VALIDATION,
|
|
96
|
+
severity: Severity.High,
|
|
97
|
+
analyzer: ANALYZER_NAME,
|
|
98
|
+
title: 'File upload without type validation',
|
|
99
|
+
description: `File upload in \`${file.path}\` does not validate the file type (missing \`mimes:\`, \`mimetypes:\`, or \`extensions:\` rule). ` +
|
|
100
|
+
`Without type validation, users can upload arbitrary files including scripts.`,
|
|
101
|
+
file: file.path,
|
|
102
|
+
line,
|
|
103
|
+
recommendation: "Add file type validation: `'file' => 'required|file|mimes:jpg,png,pdf|max:2048'`.",
|
|
104
|
+
cwe: 'CWE-434',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// 2. Missing size validation
|
|
108
|
+
if (!hasSizeValidation(snippet)) {
|
|
109
|
+
findings.push({
|
|
110
|
+
id: FINDING_NO_SIZE_VALIDATION,
|
|
111
|
+
severity: Severity.Medium,
|
|
112
|
+
analyzer: ANALYZER_NAME,
|
|
113
|
+
title: 'File upload without size validation',
|
|
114
|
+
description: `File upload in \`${file.path}\` does not validate file size (missing \`max:\` rule). ` +
|
|
115
|
+
`Without size limits, attackers can upload very large files to exhaust storage or cause denial of service.`,
|
|
116
|
+
file: file.path,
|
|
117
|
+
line,
|
|
118
|
+
recommendation: "Add a file size limit: `'file' => 'required|file|max:10240'` (in kilobytes).",
|
|
119
|
+
cwe: 'CWE-400',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// 3. Storage to public disk without access control
|
|
123
|
+
const pdRegex = /->store(?:As)?\s*\([^)]*['"]public['"]/g;
|
|
124
|
+
let pdMatch;
|
|
125
|
+
while ((pdMatch = pdRegex.exec(snippet)) !== null) {
|
|
126
|
+
findings.push({
|
|
127
|
+
id: FINDING_PUBLIC_DISK,
|
|
128
|
+
severity: Severity.Medium,
|
|
129
|
+
analyzer: ANALYZER_NAME,
|
|
130
|
+
title: 'File stored on public disk without access control',
|
|
131
|
+
description: `\`store()\` call in \`${file.path}\` uses the \`public\` disk, making files directly accessible via URL. ` +
|
|
132
|
+
`Sensitive uploaded files should not be publicly accessible without authorization checks.`,
|
|
133
|
+
file: file.path,
|
|
134
|
+
line: findLineNumber(content, block.startIndex + pdMatch.index),
|
|
135
|
+
recommendation: "Use the default (private) disk for sensitive files and serve them through an authorized controller action.",
|
|
136
|
+
cwe: 'CWE-284',
|
|
137
|
+
snippet: pdMatch[0],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// 4. Executable file extensions allowed in mimes/extensions rules
|
|
141
|
+
const rulesStr = extractValidationRules(snippet);
|
|
142
|
+
if (rulesStr) {
|
|
143
|
+
for (const ext of EXECUTABLE_EXTENSIONS) {
|
|
144
|
+
const extWithoutDot = ext.replace('.', '');
|
|
145
|
+
const extPattern = new RegExp(`(?:mimes|extensions):[^|'"]*\\b${extWithoutDot}\\b`, 'i');
|
|
146
|
+
if (extPattern.test(rulesStr)) {
|
|
147
|
+
findings.push({
|
|
148
|
+
id: FINDING_EXECUTABLE_EXTENSION,
|
|
149
|
+
severity: Severity.High,
|
|
150
|
+
analyzer: ANALYZER_NAME,
|
|
151
|
+
title: `Executable extension \`${ext}\` allowed in file upload`,
|
|
152
|
+
description: `The file upload rule in \`${file.path}\` allows the \`${ext}\` extension. ` +
|
|
153
|
+
`Allowing executable file uploads can lead to remote code execution.`,
|
|
154
|
+
file: file.path,
|
|
155
|
+
line,
|
|
156
|
+
recommendation: `Remove \`${extWithoutDot}\` from allowed extensions. Never allow PHP or other server-side executable files to be uploaded.`,
|
|
157
|
+
cwe: 'CWE-434',
|
|
158
|
+
snippet: rulesStr.slice(0, 80),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// 5. Direct file path from user input used in storage operations
|
|
165
|
+
this.checkPathFromInput(file, findings);
|
|
166
|
+
}
|
|
167
|
+
return findings;
|
|
168
|
+
}
|
|
169
|
+
checkPathFromInput(file, findings) {
|
|
170
|
+
const content = file.content;
|
|
171
|
+
const inputPathPattern = /\$request->(?:input|get)\s*\(\s*['"](?:path|filename|dir|directory|folder)['"]\s*\)/g;
|
|
172
|
+
let match;
|
|
173
|
+
while ((match = inputPathPattern.exec(content)) !== null) {
|
|
174
|
+
const surroundingCode = content.slice(Math.max(0, match.index - 200), match.index + match[0].length + 200);
|
|
175
|
+
if (/Storage::|->store|->storeAs|->move|->putFile/.test(surroundingCode)) {
|
|
176
|
+
findings.push({
|
|
177
|
+
id: FINDING_PATH_FROM_INPUT,
|
|
178
|
+
severity: Severity.High,
|
|
179
|
+
analyzer: ANALYZER_NAME,
|
|
180
|
+
title: 'File path derived from user input in storage operation',
|
|
181
|
+
description: `\`$request->input('path')\` (or similar) is used in a storage operation in \`${file.path}\`. ` +
|
|
182
|
+
`Using user-supplied paths in storage operations can lead to path traversal attacks.`,
|
|
183
|
+
file: file.path,
|
|
184
|
+
line: findLineNumber(content, match.index),
|
|
185
|
+
recommendation: 'Never use user-supplied input directly as a file path. Generate paths server-side using `Str::uuid()` or similar.',
|
|
186
|
+
cwe: 'CWE-22',
|
|
187
|
+
snippet: match[0],
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=FileUploadAnalyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileUploadAnalyzer.js","sourceRoot":"","sources":["../../src/analyzers/FileUploadAnalyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Analyzer, Finding, HavocConfig, ParsedFile } from '../types/index.js';
|
|
2
|
+
export declare class IdorAnalyzer implements Analyzer {
|
|
3
|
+
readonly name = "IdorAnalyzer";
|
|
4
|
+
readonly description = "Detects potential Insecure Direct Object Reference (IDOR) vulnerabilities";
|
|
5
|
+
analyze(files: ParsedFile[], _config: HavocConfig): Promise<Finding[]>;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=IdorAnalyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IdorAnalyzer.d.ts","sourceRoot":"","sources":["../../src/analyzers/IdorAnalyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAY,MAAM,mBAAmB,CAAC;AA4EzF,qBAAa,YAAa,YAAW,QAAQ;IAC3C,QAAQ,CAAC,IAAI,kBAAiB;IAC9B,QAAQ,CAAC,WAAW,+EAA+E;IAE7F,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAqC7E"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Severity } from '../types/index.js';
|
|
2
|
+
const NESTED_RESOURCE_PATTERN = /\{([^}]+)\}[^{]*\{([^}]+)\}/;
|
|
3
|
+
const RESOURCE_ROUTE_PATTERN = /Route::(?:resource|apiResource)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
4
|
+
const MULTI_PARAM_ROUTE_PATTERN = /Route::\w+\s*\(\s*['"]([^'"]+)['"]/g;
|
|
5
|
+
const OWNERSHIP_CHECK_PATTERNS = [
|
|
6
|
+
/->where\s*\(\s*['"]user_id['"]\s*,/,
|
|
7
|
+
/->where\s*\(\s*['"]owner_id['"]\s*,/,
|
|
8
|
+
/->whereHas\s*\(/,
|
|
9
|
+
/abort_if\s*\(/,
|
|
10
|
+
/abort_unless\s*\(/,
|
|
11
|
+
/\$this->authorize\s*\(/,
|
|
12
|
+
/Gate::/,
|
|
13
|
+
/belongsTo\s*\(/,
|
|
14
|
+
/auth\(\)->user\(\)->/,
|
|
15
|
+
];
|
|
16
|
+
const FINDING_NESTED_IDOR = 'HAVOC-IDOR-001';
|
|
17
|
+
const FINDING_RESOURCE_IDOR = 'HAVOC-IDOR-002';
|
|
18
|
+
const ANALYZER_NAME = 'IdorAnalyzer';
|
|
19
|
+
function isRouteFile(path) {
|
|
20
|
+
return (path.startsWith('routes/') ||
|
|
21
|
+
path.startsWith('routes\\') ||
|
|
22
|
+
path === 'routes/web.php' ||
|
|
23
|
+
path === 'routes/api.php');
|
|
24
|
+
}
|
|
25
|
+
function isControllerFile(path) {
|
|
26
|
+
return path.includes('Http/Controllers') || path.includes('Http\\Controllers');
|
|
27
|
+
}
|
|
28
|
+
function extractNestedRoutes(content) {
|
|
29
|
+
const routes = [];
|
|
30
|
+
const lines = content.split('\n');
|
|
31
|
+
lines.forEach((lineText, idx) => {
|
|
32
|
+
const lineNum = idx + 1;
|
|
33
|
+
// Check multi-param routes
|
|
34
|
+
const re1 = new RegExp(MULTI_PARAM_ROUTE_PATTERN.source, 'g');
|
|
35
|
+
let match;
|
|
36
|
+
while ((match = re1.exec(lineText)) !== null) {
|
|
37
|
+
const routePath = match[1];
|
|
38
|
+
if (NESTED_RESOURCE_PATTERN.test(routePath)) {
|
|
39
|
+
routes.push({ path: routePath, line: lineNum, isResource: false, routeDefinition: lineText.trim() });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Check resource routes
|
|
43
|
+
const re2 = new RegExp(RESOURCE_ROUTE_PATTERN.source, 'g');
|
|
44
|
+
while ((match = re2.exec(lineText)) !== null) {
|
|
45
|
+
const resourcePath = match[1];
|
|
46
|
+
if (resourcePath.includes('.') || resourcePath.includes('/')) {
|
|
47
|
+
routes.push({ path: resourcePath, line: lineNum, isResource: true, routeDefinition: lineText.trim() });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return routes;
|
|
52
|
+
}
|
|
53
|
+
function hasOwnershipCheck(content) {
|
|
54
|
+
return OWNERSHIP_CHECK_PATTERNS.some((re) => re.test(content));
|
|
55
|
+
}
|
|
56
|
+
export class IdorAnalyzer {
|
|
57
|
+
name = ANALYZER_NAME;
|
|
58
|
+
description = 'Detects potential Insecure Direct Object Reference (IDOR) vulnerabilities';
|
|
59
|
+
async analyze(files, _config) {
|
|
60
|
+
const findings = [];
|
|
61
|
+
const allControllerText = files
|
|
62
|
+
.filter((f) => isControllerFile(f.path))
|
|
63
|
+
.map((f) => f.content)
|
|
64
|
+
.join('\n');
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
if (!isRouteFile(file.path))
|
|
67
|
+
continue;
|
|
68
|
+
const routes = extractNestedRoutes(file.content);
|
|
69
|
+
for (const route of routes) {
|
|
70
|
+
if (!hasOwnershipCheck(allControllerText)) {
|
|
71
|
+
findings.push({
|
|
72
|
+
id: route.isResource ? FINDING_RESOURCE_IDOR : FINDING_NESTED_IDOR,
|
|
73
|
+
severity: Severity.Medium,
|
|
74
|
+
analyzer: ANALYZER_NAME,
|
|
75
|
+
title: `Potential IDOR on nested route: \`${route.path}\``,
|
|
76
|
+
description: `The route \`${route.path}\` has multiple parameters suggesting a parent-child resource relationship. ` +
|
|
77
|
+
`Without ownership verification, a user could access another user\'s child resources.`,
|
|
78
|
+
file: file.path,
|
|
79
|
+
line: route.line,
|
|
80
|
+
recommendation: 'In the controller, verify the child resource belongs to the parent. ' +
|
|
81
|
+
'Example: `$post = $user->posts()->findOrFail($postId);`',
|
|
82
|
+
cwe: 'CWE-639',
|
|
83
|
+
snippet: route.routeDefinition,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return findings;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=IdorAnalyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IdorAnalyzer.js","sourceRoot":"","sources":["../../src/analyzers/IdorAnalyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Analyzer, Finding, HavocConfig, ParsedFile } from '../types/index.js';
|
|
2
|
+
export declare class MassAssignmentAnalyzer implements Analyzer {
|
|
3
|
+
readonly name = "MassAssignmentAnalyzer";
|
|
4
|
+
readonly description = "Detects Eloquent models vulnerable to mass assignment attacks";
|
|
5
|
+
analyze(files: ParsedFile[], _config: HavocConfig): Promise<Finding[]>;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=MassAssignmentAnalyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MassAssignmentAnalyzer.d.ts","sourceRoot":"","sources":["../../src/analyzers/MassAssignmentAnalyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAY,MAAM,mBAAmB,CAAC;AAkBzF,qBAAa,sBAAuB,YAAW,QAAQ;IACrD,QAAQ,CAAC,IAAI,4BAAiB;IAC9B,QAAQ,CAAC,WAAW,mEAAmE;IAEjF,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAkF7E"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Severity } from '../types/index.js';
|
|
2
|
+
const SENSITIVE_FILLABLE_FIELDS = new Set([
|
|
3
|
+
'password', 'password_hash', 'password_confirmation', 'role', 'roles',
|
|
4
|
+
'is_admin', 'admin', 'is_superuser', 'email_verified_at', 'remember_token',
|
|
5
|
+
'api_token', 'api_key', 'stripe_id', 'two_factor_secret', 'two_factor_recovery_codes',
|
|
6
|
+
]);
|
|
7
|
+
const FINDING_NO_GUARD = 'HAVOC-MA-001';
|
|
8
|
+
const FINDING_OPEN_GUARD = 'HAVOC-MA-002';
|
|
9
|
+
const FINDING_SENSITIVE = 'HAVOC-MA-003';
|
|
10
|
+
const ANALYZER_NAME = 'MassAssignmentAnalyzer';
|
|
11
|
+
function isModelFile(path) {
|
|
12
|
+
return path.includes('app/Models') || path.includes('app\\Models');
|
|
13
|
+
}
|
|
14
|
+
export class MassAssignmentAnalyzer {
|
|
15
|
+
name = ANALYZER_NAME;
|
|
16
|
+
description = 'Detects Eloquent models vulnerable to mass assignment attacks';
|
|
17
|
+
async analyze(files, _config) {
|
|
18
|
+
const findings = [];
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
if (!isModelFile(file.path))
|
|
21
|
+
continue;
|
|
22
|
+
const phpFile = file;
|
|
23
|
+
if (!phpFile.classes)
|
|
24
|
+
continue;
|
|
25
|
+
for (const cls of phpFile.classes) {
|
|
26
|
+
if (cls.isAbstract || cls.isTrait)
|
|
27
|
+
continue;
|
|
28
|
+
const fillableProp = cls.properties.find((p) => p.name === 'fillable');
|
|
29
|
+
const guardedProp = cls.properties.find((p) => p.name === 'guarded');
|
|
30
|
+
const hasFillable = fillableProp !== undefined;
|
|
31
|
+
const hasGuarded = guardedProp !== undefined;
|
|
32
|
+
if (!hasFillable && !hasGuarded) {
|
|
33
|
+
findings.push({
|
|
34
|
+
id: FINDING_NO_GUARD,
|
|
35
|
+
severity: Severity.Medium,
|
|
36
|
+
analyzer: ANALYZER_NAME,
|
|
37
|
+
title: `Model \`${cls.name}\` has no mass assignment protection`,
|
|
38
|
+
description: `The Eloquent model \`${cls.name}\` defines neither \`$fillable\` nor \`$guarded\`. ` +
|
|
39
|
+
`Without these, all attributes may be mass-assignable.`,
|
|
40
|
+
file: file.path,
|
|
41
|
+
line: cls.line,
|
|
42
|
+
recommendation: 'Define `$fillable` with an explicit list of safe attributes, or define `$guarded = [\'id\']`.',
|
|
43
|
+
cwe: 'CWE-915',
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (hasGuarded) {
|
|
48
|
+
const initText = guardedProp?.initializer ?? '';
|
|
49
|
+
const isOpenGuard = initText.trim() === '[]';
|
|
50
|
+
if (isOpenGuard) {
|
|
51
|
+
findings.push({
|
|
52
|
+
id: FINDING_OPEN_GUARD,
|
|
53
|
+
severity: Severity.High,
|
|
54
|
+
analyzer: ANALYZER_NAME,
|
|
55
|
+
title: `Model \`${cls.name}\` uses \`$guarded = []\` (effectively unguarded)`,
|
|
56
|
+
description: `\`${cls.name}\` sets \`$guarded = []\`, which disables all mass assignment protection.`,
|
|
57
|
+
file: file.path,
|
|
58
|
+
line: guardedProp?.line ?? cls.line,
|
|
59
|
+
recommendation: 'Switch to `$fillable` with an explicit allowlist.',
|
|
60
|
+
cwe: 'CWE-915',
|
|
61
|
+
snippet: 'protected $guarded = [];',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (hasFillable) {
|
|
66
|
+
const initText = fillableProp?.initializer ?? '';
|
|
67
|
+
for (const sensitive of SENSITIVE_FILLABLE_FIELDS) {
|
|
68
|
+
if (initText.includes('"' + sensitive + '"') || initText.includes("'" + sensitive + "'")) {
|
|
69
|
+
findings.push({
|
|
70
|
+
id: FINDING_SENSITIVE,
|
|
71
|
+
severity: Severity.Medium,
|
|
72
|
+
analyzer: ANALYZER_NAME,
|
|
73
|
+
title: `Sensitive field \`${sensitive}\` is mass-assignable in \`${cls.name}\``,
|
|
74
|
+
description: `The field \`${sensitive}\` is listed in \`$fillable\` on model \`${cls.name}\`. ` +
|
|
75
|
+
`Allowing mass assignment of this field could allow privilege escalation.`,
|
|
76
|
+
file: file.path,
|
|
77
|
+
line: fillableProp?.line ?? cls.line,
|
|
78
|
+
recommendation: `Remove \`${sensitive}\` from \`$fillable\` and set it via dedicated methods.`,
|
|
79
|
+
cwe: 'CWE-915',
|
|
80
|
+
snippet: "protected $fillable = [..., '" + sensitive + "', ...];",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return findings;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=MassAssignmentAnalyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MassAssignmentAnalyzer.js","sourceRoot":"","sources":["../../src/analyzers/MassAssignmentAnalyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Analyzer, Finding, HavocConfig, ParsedFile } from '../types/index.js';
|
|
2
|
+
export declare class PrivilegeEscalationAnalyzer implements Analyzer {
|
|
3
|
+
readonly name = "PrivilegeEscalationAnalyzer";
|
|
4
|
+
readonly description = "Detects privilege escalation vectors: mass-assignable role fields, unprotected role changes, and missing admin middleware";
|
|
5
|
+
analyze(files: ParsedFile[], _config: HavocConfig): Promise<Finding[]>;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=PrivilegeEscalationAnalyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PrivilegeEscalationAnalyzer.d.ts","sourceRoot":"","sources":["../../src/analyzers/PrivilegeEscalationAnalyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAY,MAAM,mBAAmB,CAAC;AA2FzF,qBAAa,2BAA4B,YAAW,QAAQ;IAC1D,QAAQ,CAAC,IAAI,iCAAiB;IAC9B,QAAQ,CAAC,WAAW,+HAC0G;IAExH,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAiK7E"}
|