web3crit-scanner 7.0.1
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/README.md +685 -0
- package/bin/web3crit +10 -0
- package/package.json +59 -0
- package/src/analyzers/control-flow.js +256 -0
- package/src/analyzers/data-flow.js +720 -0
- package/src/analyzers/exploit-chain.js +751 -0
- package/src/analyzers/immunefi-classifier.js +515 -0
- package/src/analyzers/poc-validator.js +396 -0
- package/src/analyzers/solodit-enricher.js +1122 -0
- package/src/cli.js +546 -0
- package/src/detectors/access-control-enhanced.js +458 -0
- package/src/detectors/base-detector.js +213 -0
- package/src/detectors/callback-reentrancy.js +362 -0
- package/src/detectors/cross-contract-reentrancy.js +697 -0
- package/src/detectors/delegatecall.js +167 -0
- package/src/detectors/deprecated-functions.js +62 -0
- package/src/detectors/flash-loan.js +408 -0
- package/src/detectors/frontrunning.js +553 -0
- package/src/detectors/gas-griefing.js +701 -0
- package/src/detectors/governance-attacks.js +366 -0
- package/src/detectors/integer-overflow.js +487 -0
- package/src/detectors/oracle-manipulation.js +524 -0
- package/src/detectors/permit-exploits.js +368 -0
- package/src/detectors/precision-loss.js +408 -0
- package/src/detectors/price-manipulation-advanced.js +548 -0
- package/src/detectors/proxy-vulnerabilities.js +651 -0
- package/src/detectors/readonly-reentrancy.js +473 -0
- package/src/detectors/rebasing-token-vault.js +416 -0
- package/src/detectors/reentrancy-enhanced.js +359 -0
- package/src/detectors/selfdestruct.js +259 -0
- package/src/detectors/share-manipulation.js +412 -0
- package/src/detectors/signature-replay.js +409 -0
- package/src/detectors/storage-collision.js +446 -0
- package/src/detectors/timestamp-dependence.js +494 -0
- package/src/detectors/toctou.js +427 -0
- package/src/detectors/token-standard-compliance.js +465 -0
- package/src/detectors/unchecked-call.js +214 -0
- package/src/detectors/vault-inflation.js +421 -0
- package/src/index.js +42 -0
- package/src/package-lock.json +2874 -0
- package/src/package.json +39 -0
- package/src/scanner-enhanced.js +816 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Enhanced Access Control Detector
|
|
5
|
+
* Validates what modifiers actually do instead of just checking existence
|
|
6
|
+
* Detects broken access control logic
|
|
7
|
+
*/
|
|
8
|
+
class AccessControlEnhancedDetector extends BaseDetector {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(
|
|
11
|
+
'Access Control Vulnerability (Enhanced)',
|
|
12
|
+
'Advanced access control validation - checks modifier logic, not just presence',
|
|
13
|
+
'CRITICAL'
|
|
14
|
+
);
|
|
15
|
+
this.cfg = null;
|
|
16
|
+
this.dataFlow = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Override detect to use CFG analysis
|
|
21
|
+
*/
|
|
22
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
23
|
+
this.cfg = cfg;
|
|
24
|
+
this.dataFlow = dataFlow;
|
|
25
|
+
this.sourceCode = sourceCode;
|
|
26
|
+
this.fileName = fileName;
|
|
27
|
+
this.sourceLines = sourceCode.split('\n');
|
|
28
|
+
this.findings = [];
|
|
29
|
+
|
|
30
|
+
if (!cfg) {
|
|
31
|
+
// Fallback to basic detection
|
|
32
|
+
return super.detect(ast, sourceCode, fileName);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Analyze each function for access control issues
|
|
36
|
+
for (const [funcKey, funcInfo] of cfg.functions) {
|
|
37
|
+
this.analyzeFunctionAccessControl(funcKey, funcInfo);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Analyze modifiers for broken logic
|
|
41
|
+
for (const [modKey, modInfo] of cfg.modifiers) {
|
|
42
|
+
this.analyzeModifierLogic(modKey, modInfo);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return this.findings;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Analyze function access control
|
|
50
|
+
*/
|
|
51
|
+
analyzeFunctionAccessControl(funcKey, funcInfo) {
|
|
52
|
+
// Skip constructors
|
|
53
|
+
if (funcInfo.isConstructor) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if function needs access control
|
|
58
|
+
if (this.needsAccessControl(funcInfo)) {
|
|
59
|
+
const protection = this.analyzeAccessProtection(funcInfo);
|
|
60
|
+
|
|
61
|
+
if (protection.level === 'none') {
|
|
62
|
+
this.reportMissingAccessControl(funcInfo, protection);
|
|
63
|
+
} else if (protection.level === 'weak') {
|
|
64
|
+
this.reportWeakAccessControl(funcInfo, protection);
|
|
65
|
+
} else if (protection.level === 'broken') {
|
|
66
|
+
this.reportBrokenAccessControl(funcInfo, protection);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Determine if function needs access control
|
|
73
|
+
*/
|
|
74
|
+
needsAccessControl(funcInfo) {
|
|
75
|
+
// Private/internal functions don't need explicit access control
|
|
76
|
+
if (funcInfo.visibility === 'private' || funcInfo.visibility === 'internal') {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// View/pure functions with no state changes don't need access control
|
|
81
|
+
if ((funcInfo.stateMutability === 'view' || funcInfo.stateMutability === 'pure') &&
|
|
82
|
+
funcInfo.stateWrites.length === 0) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if function has internal access control (require/assert checks in body)
|
|
87
|
+
if (this.hasInternalAccessControl(funcInfo)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if function is a self-service withdrawal pattern (users withdraw own funds)
|
|
92
|
+
if (this.isSelfServiceWithdrawal(funcInfo)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check if function performs sensitive operations
|
|
97
|
+
return this.hasSensitiveOperations(funcInfo);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if function has internal access control checks
|
|
102
|
+
* (access control via require/assert inside function body instead of modifier)
|
|
103
|
+
*/
|
|
104
|
+
hasInternalAccessControl(funcInfo) {
|
|
105
|
+
if (!funcInfo.node || !funcInfo.node.body) return false;
|
|
106
|
+
|
|
107
|
+
const funcCode = this.getCodeSnippet(funcInfo.node.loc);
|
|
108
|
+
if (!funcCode) return false;
|
|
109
|
+
|
|
110
|
+
// Check for common internal access control patterns
|
|
111
|
+
const internalAccessPatterns = [
|
|
112
|
+
/require\s*\(\s*msg\.sender\s*==\s*\w+/i, // require(msg.sender == owner)
|
|
113
|
+
/require\s*\(\s*\w+\s*==\s*msg\.sender/i, // require(pendingOwner == msg.sender)
|
|
114
|
+
/assert\s*\(\s*msg\.sender\s*==\s*\w+/i,
|
|
115
|
+
/if\s*\(\s*msg\.sender\s*!=\s*\w+\)\s*revert/i, // if (msg.sender != owner) revert
|
|
116
|
+
/onlyOwner|onlyAdmin|onlyRole/i, // Modifier names in function
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
return internalAccessPatterns.some(pattern => pattern.test(funcCode));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if function is a self-service withdrawal pattern
|
|
124
|
+
* (users can only withdraw their own funds based on msg.sender)
|
|
125
|
+
*/
|
|
126
|
+
isSelfServiceWithdrawal(funcInfo) {
|
|
127
|
+
const funcNameLower = funcInfo.name.toLowerCase().replace(/[_\s]/g, '');
|
|
128
|
+
|
|
129
|
+
// Check if function name suggests self-service pattern
|
|
130
|
+
const selfServiceNames = ['withdraw', 'withdrawfunds', 'claim', 'redeem', 'collect'];
|
|
131
|
+
if (!selfServiceNames.some(name => funcNameLower.includes(name))) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!funcInfo.node || !funcInfo.node.body) return false;
|
|
136
|
+
|
|
137
|
+
const funcCode = this.getCodeSnippet(funcInfo.node.loc);
|
|
138
|
+
if (!funcCode) return false;
|
|
139
|
+
|
|
140
|
+
// Check if function accesses user-specific storage using msg.sender
|
|
141
|
+
// e.g., balances[msg.sender] or pendingWithdrawals[msg.sender]
|
|
142
|
+
const selfServicePatterns = [
|
|
143
|
+
/\[\s*msg\.sender\s*\]/, // mapping[msg.sender]
|
|
144
|
+
/balances\s*\[\s*msg\.sender\s*\]/i,
|
|
145
|
+
/pending\w*\s*\[\s*msg\.sender\s*\]/i,
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
return selfServicePatterns.some(pattern => pattern.test(funcCode));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if function performs sensitive operations
|
|
153
|
+
*/
|
|
154
|
+
hasSensitiveOperations(funcInfo) {
|
|
155
|
+
// Check for dangerous operations
|
|
156
|
+
const dangerousOps = ['delegatecall', 'selfdestruct', 'suicide'];
|
|
157
|
+
if (funcInfo.externalCalls.some(call => dangerousOps.includes(call.type))) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check for direct value-moving operations by inspecting code for transfer/mint/burn patterns.
|
|
162
|
+
// This is intentionally conservative to avoid name-only false positives (e.g., withdrawInfo()).
|
|
163
|
+
if (funcInfo.node && funcInfo.node.loc) {
|
|
164
|
+
const code = (this.getCodeSnippet(funcInfo.node.loc) || '').toLowerCase();
|
|
165
|
+
const valueMovingPatterns = [
|
|
166
|
+
/\.transfer\s*\(/, // ERC20/ETH transfer-like
|
|
167
|
+
/\.transferfrom\s*\(/, // ERC20 transferFrom
|
|
168
|
+
/\.send\s*\(/, // ETH send
|
|
169
|
+
/\.call\s*\{[^}]*value/i, // call{value: ...}
|
|
170
|
+
/\b_mint\s*\(/,
|
|
171
|
+
/\bmint\s*\(/,
|
|
172
|
+
/\b_burn\s*\(/,
|
|
173
|
+
/\bburn\s*\(/,
|
|
174
|
+
/\bselfdestruct\s*\(/,
|
|
175
|
+
];
|
|
176
|
+
if (valueMovingPatterns.some(p => p.test(code))) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check if modifies critical state
|
|
182
|
+
if (this.modifiesCriticalState(funcInfo)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if function modifies critical state variables
|
|
191
|
+
*/
|
|
192
|
+
modifiesCriticalState(funcInfo) {
|
|
193
|
+
const criticalVars = ['owner', 'admin', 'paused', 'implementation'];
|
|
194
|
+
|
|
195
|
+
return funcInfo.stateWrites.some(write => {
|
|
196
|
+
const varName = write.variable.toLowerCase();
|
|
197
|
+
return criticalVars.some(cv => varName.includes(cv));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Analyze what access protection a function has
|
|
203
|
+
*/
|
|
204
|
+
analyzeAccessProtection(funcInfo) {
|
|
205
|
+
if (funcInfo.modifiers.length === 0) {
|
|
206
|
+
return {
|
|
207
|
+
level: 'none',
|
|
208
|
+
reason: 'No access control modifiers'
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Analyze each modifier
|
|
213
|
+
const modifierAnalysis = funcInfo.modifiers.map(modName => {
|
|
214
|
+
const modKey = `${funcInfo.contract}.${modName}`;
|
|
215
|
+
const modInfo = this.cfg.modifiers.get(modKey);
|
|
216
|
+
|
|
217
|
+
if (!modInfo) {
|
|
218
|
+
return {
|
|
219
|
+
name: modName,
|
|
220
|
+
effective: 'unknown',
|
|
221
|
+
reason: 'Modifier not found in contract'
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return this.evaluateModifierEffectiveness(modInfo);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Determine overall protection level
|
|
229
|
+
const hasEffectiveProtection = modifierAnalysis.some(m => m.effective === 'strong');
|
|
230
|
+
const hasBrokenProtection = modifierAnalysis.some(m => m.effective === 'broken');
|
|
231
|
+
const hasWeakProtection = modifierAnalysis.some(m => m.effective === 'weak');
|
|
232
|
+
|
|
233
|
+
if (hasBrokenProtection) {
|
|
234
|
+
return {
|
|
235
|
+
level: 'broken',
|
|
236
|
+
modifiers: modifierAnalysis,
|
|
237
|
+
reason: 'Modifier exists but logic is broken'
|
|
238
|
+
};
|
|
239
|
+
} else if (hasEffectiveProtection) {
|
|
240
|
+
return {
|
|
241
|
+
level: 'strong',
|
|
242
|
+
modifiers: modifierAnalysis
|
|
243
|
+
};
|
|
244
|
+
} else if (hasWeakProtection) {
|
|
245
|
+
return {
|
|
246
|
+
level: 'weak',
|
|
247
|
+
modifiers: modifierAnalysis,
|
|
248
|
+
reason: 'Modifier provides weak protection'
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
level: 'none',
|
|
254
|
+
modifiers: modifierAnalysis,
|
|
255
|
+
reason: 'Modifiers do not provide access control'
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Evaluate how effective a modifier is at access control
|
|
261
|
+
*/
|
|
262
|
+
evaluateModifierEffectiveness(modInfo) {
|
|
263
|
+
if (!modInfo.checksAccess) {
|
|
264
|
+
return {
|
|
265
|
+
name: modInfo.name,
|
|
266
|
+
effective: 'none',
|
|
267
|
+
reason: 'Modifier does not check access'
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check for common broken patterns
|
|
272
|
+
const brokenPatterns = this.detectBrokenPatterns(modInfo);
|
|
273
|
+
if (brokenPatterns.length > 0) {
|
|
274
|
+
return {
|
|
275
|
+
name: modInfo.name,
|
|
276
|
+
effective: 'broken',
|
|
277
|
+
reason: `Broken pattern: ${brokenPatterns.join(', ')}`
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check for weak patterns
|
|
282
|
+
const weakPatterns = this.detectWeakPatterns(modInfo);
|
|
283
|
+
if (weakPatterns.length > 0) {
|
|
284
|
+
return {
|
|
285
|
+
name: modInfo.name,
|
|
286
|
+
effective: 'weak',
|
|
287
|
+
reason: `Weak pattern: ${weakPatterns.join(', ')}`
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Modifier appears to provide strong protection
|
|
292
|
+
return {
|
|
293
|
+
name: modInfo.name,
|
|
294
|
+
effective: 'strong',
|
|
295
|
+
reason: modInfo.checksOwnership ? 'Checks ownership' :
|
|
296
|
+
modInfo.checksRole ? 'Checks role' :
|
|
297
|
+
'Checks access control'
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Detect broken access control patterns
|
|
303
|
+
*/
|
|
304
|
+
detectBrokenPatterns(modInfo) {
|
|
305
|
+
const broken = [];
|
|
306
|
+
|
|
307
|
+
// Check if modifier has empty body
|
|
308
|
+
if (modInfo.requireStatements.length === 0) {
|
|
309
|
+
broken.push('Empty modifier - no checks');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check for always-true conditions
|
|
313
|
+
modInfo.requireStatements.forEach(stmt => {
|
|
314
|
+
if (stmt === 'true' || stmt === '1 == 1') {
|
|
315
|
+
broken.push('Always-true condition');
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Check for tx.origin instead of msg.sender (phishing risk)
|
|
320
|
+
modInfo.requireStatements.forEach(stmt => {
|
|
321
|
+
if (stmt.includes('tx.origin') && !stmt.includes('msg.sender')) {
|
|
322
|
+
broken.push('Uses tx.origin (vulnerable to phishing)');
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return broken;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Detect weak access control patterns
|
|
331
|
+
*/
|
|
332
|
+
detectWeakPatterns(modInfo) {
|
|
333
|
+
const weak = [];
|
|
334
|
+
|
|
335
|
+
// Check for balance-based access control (flash loan vulnerable)
|
|
336
|
+
modInfo.requireStatements.forEach(stmt => {
|
|
337
|
+
if (stmt.includes('balanceOf') || stmt.includes('.balance >')) {
|
|
338
|
+
weak.push('Balance-based access control (flash loan risk)');
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Check for timestamp-based access (miner manipulation)
|
|
343
|
+
modInfo.requireStatements.forEach(stmt => {
|
|
344
|
+
if (stmt.includes('block.timestamp') || stmt.includes('now')) {
|
|
345
|
+
weak.push('Timestamp-based check (miner manipulation risk)');
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return weak;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Analyze modifier logic for vulnerabilities
|
|
354
|
+
*/
|
|
355
|
+
analyzeModifierLogic(modKey, modInfo) {
|
|
356
|
+
// Check for modifier that claims to provide access control but doesn't
|
|
357
|
+
const broken = this.detectBrokenPatterns(modInfo);
|
|
358
|
+
|
|
359
|
+
if (broken.length > 0) {
|
|
360
|
+
this.reportBrokenModifier(modInfo, broken);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Report missing access control
|
|
366
|
+
*/
|
|
367
|
+
reportMissingAccessControl(funcInfo, protection) {
|
|
368
|
+
this.addFinding({
|
|
369
|
+
title: 'Missing Access Control on Sensitive Function',
|
|
370
|
+
description: `Function '${funcInfo.name}' performs sensitive operations without access control. ${protection.reason}. This allows ANY user to call this function.`,
|
|
371
|
+
location: `Contract: ${funcInfo.contract}, Function: ${funcInfo.name}`,
|
|
372
|
+
line: funcInfo.node.loc ? funcInfo.node.loc.start.line : 0,
|
|
373
|
+
column: funcInfo.node.loc ? funcInfo.node.loc.start.column : 0,
|
|
374
|
+
code: this.getCodeSnippet(funcInfo.node.loc),
|
|
375
|
+
severity: 'CRITICAL',
|
|
376
|
+
confidence: 'HIGH',
|
|
377
|
+
exploitable: true,
|
|
378
|
+
recommendation: 'Add access control modifier (e.g., onlyOwner) to restrict who can call this function. Use OpenZeppelin Ownable or AccessControl contracts.',
|
|
379
|
+
references: [
|
|
380
|
+
'https://docs.openzeppelin.com/contracts/4.x/access-control',
|
|
381
|
+
'https://swcregistry.io/docs/SWC-105'
|
|
382
|
+
]
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Report weak access control
|
|
388
|
+
*/
|
|
389
|
+
reportWeakAccessControl(funcInfo, protection) {
|
|
390
|
+
const modifierDetails = protection.modifiers.map(m =>
|
|
391
|
+
`${m.name}: ${m.reason}`
|
|
392
|
+
).join('; ');
|
|
393
|
+
|
|
394
|
+
this.addFinding({
|
|
395
|
+
title: 'Weak Access Control Pattern',
|
|
396
|
+
description: `Function '${funcInfo.name}' uses weak access control that can be bypassed. ${modifierDetails}. This may be exploitable.`,
|
|
397
|
+
location: `Contract: ${funcInfo.contract}, Function: ${funcInfo.name}`,
|
|
398
|
+
line: funcInfo.node.loc ? funcInfo.node.loc.start.line : 0,
|
|
399
|
+
column: funcInfo.node.loc ? funcInfo.node.loc.start.column : 0,
|
|
400
|
+
code: this.getCodeSnippet(funcInfo.node.loc),
|
|
401
|
+
severity: 'HIGH',
|
|
402
|
+
confidence: 'MEDIUM',
|
|
403
|
+
exploitable: true,
|
|
404
|
+
recommendation: 'Replace weak access control with proper ownership or role-based checks. Avoid balance-based or timestamp-based access control.',
|
|
405
|
+
references: [
|
|
406
|
+
'https://docs.openzeppelin.com/contracts/4.x/access-control'
|
|
407
|
+
]
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Report broken access control
|
|
413
|
+
*/
|
|
414
|
+
reportBrokenAccessControl(funcInfo, protection) {
|
|
415
|
+
const modifierDetails = protection.modifiers.map(m =>
|
|
416
|
+
`${m.name}: ${m.reason}`
|
|
417
|
+
).join('; ');
|
|
418
|
+
|
|
419
|
+
this.addFinding({
|
|
420
|
+
title: 'Broken Access Control - Logic Error',
|
|
421
|
+
description: `Function '${funcInfo.name}' has access control modifier BUT it contains logic errors that make it ineffective. ${modifierDetails}. CRITICAL: Access control appears to exist but does not work.`,
|
|
422
|
+
location: `Contract: ${funcInfo.contract}, Function: ${funcInfo.name}`,
|
|
423
|
+
line: funcInfo.node.loc ? funcInfo.node.loc.start.line : 0,
|
|
424
|
+
column: funcInfo.node.loc ? funcInfo.node.loc.start.column : 0,
|
|
425
|
+
code: this.getCodeSnippet(funcInfo.node.loc),
|
|
426
|
+
severity: 'CRITICAL',
|
|
427
|
+
confidence: 'HIGH',
|
|
428
|
+
exploitable: true,
|
|
429
|
+
recommendation: 'Fix the modifier logic immediately. This is more dangerous than missing access control because developers may assume the function is protected.',
|
|
430
|
+
references: [
|
|
431
|
+
'https://docs.openzeppelin.com/contracts/4.x/access-control'
|
|
432
|
+
]
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Report broken modifier
|
|
438
|
+
*/
|
|
439
|
+
reportBrokenModifier(modInfo, issues) {
|
|
440
|
+
this.addFinding({
|
|
441
|
+
title: 'Broken Access Control Modifier',
|
|
442
|
+
description: `Modifier '${modInfo.name}' contains logic errors: ${issues.join(', ')}. Functions using this modifier are NOT protected.`,
|
|
443
|
+
location: `Contract: ${modInfo.contract}, Modifier: ${modInfo.name}`,
|
|
444
|
+
line: modInfo.node.loc ? modInfo.node.loc.start.line : 0,
|
|
445
|
+
column: modInfo.node.loc ? modInfo.node.loc.start.column : 0,
|
|
446
|
+
code: this.getCodeSnippet(modInfo.node.loc),
|
|
447
|
+
severity: 'CRITICAL',
|
|
448
|
+
confidence: 'HIGH',
|
|
449
|
+
exploitable: true,
|
|
450
|
+
recommendation: 'Fix modifier logic immediately. All functions using this modifier are vulnerable.',
|
|
451
|
+
references: [
|
|
452
|
+
'https://docs.soliditylang.org/en/latest/contracts.html#function-modifiers'
|
|
453
|
+
]
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
module.exports = AccessControlEnhancedDetector;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Detector Class (Enhanced)
|
|
3
|
+
* Provides foundation for all vulnerability detectors with:
|
|
4
|
+
* - Exploitability scoring (0-100)
|
|
5
|
+
* - Attack vector classification
|
|
6
|
+
* - Foundry PoC support for high-confidence findings
|
|
7
|
+
*/
|
|
8
|
+
class BaseDetector {
|
|
9
|
+
constructor(name, description, severity) {
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.description = description;
|
|
12
|
+
this.severity = severity;
|
|
13
|
+
this.findings = [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
17
|
+
this.findings = [];
|
|
18
|
+
this.ast = ast;
|
|
19
|
+
this.sourceCode = sourceCode;
|
|
20
|
+
this.fileName = fileName;
|
|
21
|
+
this.sourceLines = sourceCode.split('\n');
|
|
22
|
+
this.cfg = cfg;
|
|
23
|
+
this.dataFlow = dataFlow;
|
|
24
|
+
|
|
25
|
+
// Traverse the AST
|
|
26
|
+
this.traverse(ast);
|
|
27
|
+
|
|
28
|
+
return this.findings;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
traverse(node) {
|
|
32
|
+
if (!node) return;
|
|
33
|
+
|
|
34
|
+
// Visit the node
|
|
35
|
+
const methodName = `visit${node.type}`;
|
|
36
|
+
if (typeof this[methodName] === 'function') {
|
|
37
|
+
this[methodName](node);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Traverse children
|
|
41
|
+
for (const key in node) {
|
|
42
|
+
if (key === 'loc' || key === 'range') continue;
|
|
43
|
+
|
|
44
|
+
const child = node[key];
|
|
45
|
+
if (Array.isArray(child)) {
|
|
46
|
+
child.forEach(c => this.traverse(c));
|
|
47
|
+
} else if (child && typeof child === 'object' && child.type) {
|
|
48
|
+
this.traverse(child);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Add a vulnerability finding with enhanced classification
|
|
55
|
+
*
|
|
56
|
+
* @param {Object} vulnerability - Finding details
|
|
57
|
+
* @param {string} vulnerability.title - Short title
|
|
58
|
+
* @param {string} vulnerability.description - Detailed description
|
|
59
|
+
* @param {string} vulnerability.severity - CRITICAL, HIGH, MEDIUM, LOW, INFO
|
|
60
|
+
* @param {string} vulnerability.confidence - HIGH, MEDIUM, LOW
|
|
61
|
+
* @param {boolean} vulnerability.exploitable - Is this realistically exploitable
|
|
62
|
+
* @param {number} vulnerability.exploitabilityScore - 0-100 score (optional)
|
|
63
|
+
* @param {string} vulnerability.attackVector - Attack classification (optional)
|
|
64
|
+
* @param {string} vulnerability.foundryPoC - Foundry test code for high-confidence findings (optional)
|
|
65
|
+
*/
|
|
66
|
+
addFinding(vulnerability) {
|
|
67
|
+
// Calculate exploitability score if not provided
|
|
68
|
+
const exploitabilityScore = vulnerability.exploitabilityScore ||
|
|
69
|
+
this.calculateExploitabilityScore(vulnerability);
|
|
70
|
+
|
|
71
|
+
// Determine if this is a high-confidence finding worthy of PoC
|
|
72
|
+
const isHighConfidence = this.isHighConfidenceFinding(vulnerability, exploitabilityScore);
|
|
73
|
+
|
|
74
|
+
this.findings.push({
|
|
75
|
+
detector: this.name,
|
|
76
|
+
severity: vulnerability.severity || this.severity,
|
|
77
|
+
confidence: vulnerability.confidence || 'MEDIUM',
|
|
78
|
+
exploitable: vulnerability.exploitable !== undefined ? vulnerability.exploitable : true,
|
|
79
|
+
exploitabilityScore: exploitabilityScore,
|
|
80
|
+
attackVector: vulnerability.attackVector || this.classifyAttackVector(vulnerability),
|
|
81
|
+
title: vulnerability.title,
|
|
82
|
+
description: vulnerability.description,
|
|
83
|
+
location: vulnerability.location,
|
|
84
|
+
fileName: this.fileName,
|
|
85
|
+
line: vulnerability.line,
|
|
86
|
+
column: vulnerability.column,
|
|
87
|
+
code: vulnerability.code,
|
|
88
|
+
recommendation: vulnerability.recommendation,
|
|
89
|
+
references: vulnerability.references || [],
|
|
90
|
+
// Only include PoC for high-confidence findings
|
|
91
|
+
foundryPoC: isHighConfidence ? vulnerability.foundryPoC : undefined,
|
|
92
|
+
isHighConfidence: isHighConfidence
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Calculate exploitability score based on finding characteristics
|
|
98
|
+
* Score: 0-100 where higher = more likely exploitable
|
|
99
|
+
*
|
|
100
|
+
* HARDENED: More aggressive scoring to filter noise
|
|
101
|
+
* - Require HIGH confidence for high scores
|
|
102
|
+
* - Penalize theoretical/best-practice issues
|
|
103
|
+
* - Bonus for concrete attack vectors
|
|
104
|
+
*/
|
|
105
|
+
calculateExploitabilityScore(vulnerability) {
|
|
106
|
+
let score = 40; // Base score (lowered from 50)
|
|
107
|
+
|
|
108
|
+
// Severity impact (unchanged)
|
|
109
|
+
const severityScores = {
|
|
110
|
+
'CRITICAL': 30,
|
|
111
|
+
'HIGH': 20,
|
|
112
|
+
'MEDIUM': 5, // Reduced from 10
|
|
113
|
+
'LOW': -10, // Penalty
|
|
114
|
+
'INFO': -30 // Stronger penalty
|
|
115
|
+
};
|
|
116
|
+
score += severityScores[vulnerability.severity] || 0;
|
|
117
|
+
|
|
118
|
+
// Confidence impact (increased importance)
|
|
119
|
+
const confidenceScores = {
|
|
120
|
+
'HIGH': 25, // Increased from 20
|
|
121
|
+
'MEDIUM': 0,
|
|
122
|
+
'LOW': -25 // Stronger penalty
|
|
123
|
+
};
|
|
124
|
+
score += confidenceScores[vulnerability.confidence] || 0;
|
|
125
|
+
|
|
126
|
+
// Exploitable flag (stronger impact)
|
|
127
|
+
if (vulnerability.exploitable === false) {
|
|
128
|
+
score -= 40; // Increased penalty
|
|
129
|
+
} else if (vulnerability.exploitable === true) {
|
|
130
|
+
score += 10; // Bonus for confirmed exploitable
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Bonus for concrete attack vector (not 'unknown')
|
|
134
|
+
if (vulnerability.attackVector && vulnerability.attackVector !== 'unknown') {
|
|
135
|
+
score += 5;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Bonus for having Foundry PoC
|
|
139
|
+
if (vulnerability.foundryPoC) {
|
|
140
|
+
score += 10;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Clamp to 0-100
|
|
144
|
+
return Math.max(0, Math.min(100, score));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Determine if finding is high-confidence (worthy of PoC)
|
|
149
|
+
*/
|
|
150
|
+
isHighConfidenceFinding(vulnerability, exploitabilityScore) {
|
|
151
|
+
// Must have high confidence AND be exploitable AND score >= 70
|
|
152
|
+
if (vulnerability.confidence !== 'HIGH') return false;
|
|
153
|
+
if (vulnerability.exploitable === false) return false;
|
|
154
|
+
if (exploitabilityScore < 70) return false;
|
|
155
|
+
|
|
156
|
+
// CRITICAL or HIGH severity
|
|
157
|
+
const highSeverity = ['CRITICAL', 'HIGH'].includes(vulnerability.severity);
|
|
158
|
+
if (!highSeverity) return false;
|
|
159
|
+
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Classify the attack vector based on finding characteristics
|
|
165
|
+
*/
|
|
166
|
+
classifyAttackVector(vulnerability) {
|
|
167
|
+
const title = (vulnerability.title || '').toLowerCase();
|
|
168
|
+
const desc = (vulnerability.description || '').toLowerCase();
|
|
169
|
+
const combined = title + ' ' + desc;
|
|
170
|
+
|
|
171
|
+
// Attack vector classification
|
|
172
|
+
const vectors = [
|
|
173
|
+
{ pattern: /reentrancy|reentrant/i, vector: 'reentrancy' },
|
|
174
|
+
{ pattern: /overflow|underflow/i, vector: 'integer-overflow' },
|
|
175
|
+
{ pattern: /flash.?loan|oracle.?manipul/i, vector: 'flash-loan' },
|
|
176
|
+
{ pattern: /front.?run|sandwich|mev/i, vector: 'frontrunning' },
|
|
177
|
+
{ pattern: /access.?control|unauthorized|permission/i, vector: 'access-control' },
|
|
178
|
+
{ pattern: /dos|denial|gas.?grief/i, vector: 'denial-of-service' },
|
|
179
|
+
{ pattern: /timestamp|block\.number/i, vector: 'timestamp-manipulation' },
|
|
180
|
+
{ pattern: /delegate.?call/i, vector: 'delegatecall' },
|
|
181
|
+
{ pattern: /selfdestruct|self.?destruct/i, vector: 'selfdestruct' },
|
|
182
|
+
{ pattern: /signature|replay/i, vector: 'signature-replay' },
|
|
183
|
+
{ pattern: /unchecked.?call|call.?return/i, vector: 'unchecked-call' }
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
for (const { pattern, vector } of vectors) {
|
|
187
|
+
if (pattern.test(combined)) {
|
|
188
|
+
return vector;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return 'unknown';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
getCodeSnippet(loc) {
|
|
196
|
+
if (!loc || !loc.start) return '';
|
|
197
|
+
|
|
198
|
+
const startLine = loc.start.line - 1;
|
|
199
|
+
const endLine = loc.end ? loc.end.line - 1 : startLine;
|
|
200
|
+
|
|
201
|
+
return this.sourceLines.slice(
|
|
202
|
+
Math.max(0, startLine),
|
|
203
|
+
Math.min(this.sourceLines.length, endLine + 1)
|
|
204
|
+
).join('\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getLineContent(lineNumber) {
|
|
208
|
+
if (lineNumber < 1 || lineNumber > this.sourceLines.length) return '';
|
|
209
|
+
return this.sourceLines[lineNumber - 1];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = BaseDetector;
|