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,366 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Governance Attack Detector
|
|
5
|
+
* Detects vulnerabilities in governance mechanisms that can lead to:
|
|
6
|
+
* - Governance takeover via flash loan voting
|
|
7
|
+
* - Proposal hijacking
|
|
8
|
+
* - Vote manipulation
|
|
9
|
+
* - Timelock bypass
|
|
10
|
+
*
|
|
11
|
+
* Immunefi Critical: Governance takeover = full protocol control
|
|
12
|
+
*/
|
|
13
|
+
class GovernanceAttackDetector extends BaseDetector {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(
|
|
16
|
+
'Governance Attack',
|
|
17
|
+
'Detects governance vulnerabilities exploitable for protocol takeover',
|
|
18
|
+
'CRITICAL'
|
|
19
|
+
);
|
|
20
|
+
this.currentContract = null;
|
|
21
|
+
this.currentFunction = null;
|
|
22
|
+
this.governancePatterns = [];
|
|
23
|
+
this.votingMechanisms = [];
|
|
24
|
+
this.timelocks = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
28
|
+
this.findings = [];
|
|
29
|
+
this.ast = ast;
|
|
30
|
+
this.sourceCode = sourceCode;
|
|
31
|
+
this.fileName = fileName;
|
|
32
|
+
this.sourceLines = sourceCode.split('\n');
|
|
33
|
+
this.cfg = cfg;
|
|
34
|
+
this.dataFlow = dataFlow;
|
|
35
|
+
this.governancePatterns = [];
|
|
36
|
+
this.votingMechanisms = [];
|
|
37
|
+
this.timelocks = [];
|
|
38
|
+
|
|
39
|
+
this.traverse(ast);
|
|
40
|
+
this.analyzeGovernancePatterns();
|
|
41
|
+
|
|
42
|
+
return this.findings;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
visitContractDefinition(node) {
|
|
46
|
+
this.currentContract = node.name;
|
|
47
|
+
|
|
48
|
+
// Check if this is a governance contract
|
|
49
|
+
const baseContracts = (node.baseContracts || []).map(b =>
|
|
50
|
+
b.baseName?.namePath || ''
|
|
51
|
+
).join(' ');
|
|
52
|
+
|
|
53
|
+
if (/Governor|Governance|DAO|Voting|Timelock/i.test(this.currentContract) ||
|
|
54
|
+
/Governor|Governance|DAO|Voting|Timelock/i.test(baseContracts)) {
|
|
55
|
+
this.governancePatterns.push({
|
|
56
|
+
contract: this.currentContract,
|
|
57
|
+
type: 'governance_contract',
|
|
58
|
+
node: node
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
visitFunctionDefinition(node) {
|
|
64
|
+
this.currentFunction = node.name || 'constructor';
|
|
65
|
+
|
|
66
|
+
if (!node.body) return;
|
|
67
|
+
|
|
68
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
69
|
+
const funcName = (node.name || '').toLowerCase();
|
|
70
|
+
|
|
71
|
+
// Skip internal functions
|
|
72
|
+
if (node.visibility === 'private' || node.visibility === 'internal') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Detect voting functions
|
|
77
|
+
this.detectVotingVulnerabilities(funcCode, node, funcName);
|
|
78
|
+
|
|
79
|
+
// Detect proposal functions
|
|
80
|
+
this.detectProposalVulnerabilities(funcCode, node, funcName);
|
|
81
|
+
|
|
82
|
+
// Detect timelock issues
|
|
83
|
+
this.detectTimelockVulnerabilities(funcCode, node, funcName);
|
|
84
|
+
|
|
85
|
+
// Detect flash loan governance
|
|
86
|
+
this.detectFlashLoanGovernance(funcCode, node, funcName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detect voting mechanism vulnerabilities
|
|
91
|
+
*/
|
|
92
|
+
detectVotingVulnerabilities(funcCode, node, funcName) {
|
|
93
|
+
// Flash loan voting - voting power from current balance
|
|
94
|
+
if (/vote|castVote/i.test(funcName)) {
|
|
95
|
+
// Check if voting power is from current balance (flash loan vulnerable)
|
|
96
|
+
if (/balanceOf\s*\(|getVotes\s*\(/.test(funcCode)) {
|
|
97
|
+
// Check for snapshot protection
|
|
98
|
+
const hasSnapshot = /getPastVotes|getPastTotalSupply|snapshot|checkpoint/i.test(funcCode);
|
|
99
|
+
const hasBlockDelay = /block\.number\s*-|votingDelay|proposalSnapshot/i.test(funcCode);
|
|
100
|
+
|
|
101
|
+
if (!hasSnapshot && !hasBlockDelay) {
|
|
102
|
+
this.addFinding({
|
|
103
|
+
title: 'Flash Loan Governance Attack',
|
|
104
|
+
description: `Function '${this.currentFunction}' uses current balance for voting power without snapshot protection. Attacker can:
|
|
105
|
+
1. Take flash loan of governance tokens
|
|
106
|
+
2. Vote with borrowed voting power
|
|
107
|
+
3. Return tokens in same transaction
|
|
108
|
+
4. Pass malicious proposal with temporary supermajority
|
|
109
|
+
|
|
110
|
+
This enables full governance takeover with zero capital.`,
|
|
111
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
112
|
+
line: node.loc?.start?.line || 0,
|
|
113
|
+
column: node.loc?.start?.column || 0,
|
|
114
|
+
code: funcCode.substring(0, 300),
|
|
115
|
+
severity: 'CRITICAL',
|
|
116
|
+
confidence: 'HIGH',
|
|
117
|
+
exploitable: true,
|
|
118
|
+
exploitabilityScore: 95,
|
|
119
|
+
attackVector: 'flash-loan-governance',
|
|
120
|
+
recommendation: `Implement snapshot-based voting:
|
|
121
|
+
1. Use ERC20Votes with getPastVotes(account, blockNumber)
|
|
122
|
+
2. Snapshot voting power at proposal creation time
|
|
123
|
+
3. Add voting delay (proposalSnapshot = block.number + votingDelay)
|
|
124
|
+
4. Consider vote escrow (veToken) requiring time-locked tokens`,
|
|
125
|
+
references: [
|
|
126
|
+
'https://www.comp.xyz/t/flash-loan-governance-attacks/2289',
|
|
127
|
+
'https://docs.openzeppelin.com/contracts/4.x/api/governance'
|
|
128
|
+
],
|
|
129
|
+
foundryPoC: this.generateFlashLoanGovernancePoC()
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Vote delegation manipulation
|
|
135
|
+
if (/delegate|delegatee/i.test(funcCode)) {
|
|
136
|
+
if (!/block\.number|snapshot|checkpoint/i.test(funcCode)) {
|
|
137
|
+
this.addFinding({
|
|
138
|
+
title: 'Delegation Manipulation Risk',
|
|
139
|
+
description: `Vote delegation in '${this.currentFunction}' may allow manipulation. Attackers can delegate/undelegate around snapshot times to double-count votes or avoid dilution.`,
|
|
140
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
141
|
+
line: node.loc?.start?.line || 0,
|
|
142
|
+
code: funcCode.substring(0, 200),
|
|
143
|
+
severity: 'HIGH',
|
|
144
|
+
confidence: 'MEDIUM',
|
|
145
|
+
exploitable: true,
|
|
146
|
+
exploitabilityScore: 70,
|
|
147
|
+
attackVector: 'delegation-manipulation',
|
|
148
|
+
recommendation: 'Use checkpointed delegation with historical lookups. Ensure delegation changes are reflected in past vote calculations.'
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Detect proposal mechanism vulnerabilities
|
|
157
|
+
*/
|
|
158
|
+
detectProposalVulnerabilities(funcCode, node, funcName) {
|
|
159
|
+
if (/propose|createProposal|submitProposal/i.test(funcName)) {
|
|
160
|
+
// Check proposal threshold
|
|
161
|
+
const hasThreshold = /proposalThreshold|minProposerBalance|require.*balance/i.test(funcCode);
|
|
162
|
+
|
|
163
|
+
if (!hasThreshold) {
|
|
164
|
+
this.addFinding({
|
|
165
|
+
title: 'Missing Proposal Threshold',
|
|
166
|
+
description: `Function '${this.currentFunction}' allows creating proposals without minimum token threshold. Attacker can spam proposals or create malicious proposals with dust amounts.`,
|
|
167
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
168
|
+
line: node.loc?.start?.line || 0,
|
|
169
|
+
code: funcCode.substring(0, 200),
|
|
170
|
+
severity: 'MEDIUM',
|
|
171
|
+
confidence: 'HIGH',
|
|
172
|
+
exploitable: true,
|
|
173
|
+
exploitabilityScore: 60,
|
|
174
|
+
attackVector: 'proposal-spam',
|
|
175
|
+
recommendation: 'Require minimum token balance or stake to create proposals: require(getVotes(msg.sender) >= proposalThreshold)'
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for arbitrary execution
|
|
180
|
+
if (/\.call\s*\(|delegatecall|target.*data/i.test(funcCode)) {
|
|
181
|
+
if (!/timelock|delay|queue/i.test(funcCode)) {
|
|
182
|
+
this.addFinding({
|
|
183
|
+
title: 'Proposal Execution Without Timelock',
|
|
184
|
+
description: `Proposals in '${this.currentFunction}' may execute immediately without timelock. Malicious proposals could drain funds before users can react.`,
|
|
185
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
186
|
+
line: node.loc?.start?.line || 0,
|
|
187
|
+
code: funcCode.substring(0, 200),
|
|
188
|
+
severity: 'HIGH',
|
|
189
|
+
confidence: 'MEDIUM',
|
|
190
|
+
exploitable: true,
|
|
191
|
+
exploitabilityScore: 75,
|
|
192
|
+
attackVector: 'instant-governance',
|
|
193
|
+
recommendation: 'Add mandatory timelock delay between proposal passing and execution: require(block.timestamp >= proposal.eta)'
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Execute function vulnerabilities
|
|
200
|
+
if (/execute|executeProposal/i.test(funcName)) {
|
|
201
|
+
// Check for reentrancy in execution
|
|
202
|
+
if (/\.call\s*\(|\.transfer\s*\(/.test(funcCode)) {
|
|
203
|
+
if (!/nonReentrant|_status|locked/i.test(funcCode)) {
|
|
204
|
+
this.addFinding({
|
|
205
|
+
title: 'Governance Execution Reentrancy',
|
|
206
|
+
description: `Proposal execution in '${this.currentFunction}' makes external calls without reentrancy protection. Malicious proposal targets could reenter and manipulate governance state.`,
|
|
207
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
208
|
+
line: node.loc?.start?.line || 0,
|
|
209
|
+
code: funcCode.substring(0, 200),
|
|
210
|
+
severity: 'HIGH',
|
|
211
|
+
confidence: 'HIGH',
|
|
212
|
+
exploitable: true,
|
|
213
|
+
exploitabilityScore: 80,
|
|
214
|
+
attackVector: 'governance-reentrancy',
|
|
215
|
+
recommendation: 'Add nonReentrant modifier to proposal execution functions.'
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Detect timelock vulnerabilities
|
|
224
|
+
*/
|
|
225
|
+
detectTimelockVulnerabilities(funcCode, node, funcName) {
|
|
226
|
+
// Emergency bypass
|
|
227
|
+
if (/emergency|bypass|skip.*delay/i.test(funcName)) {
|
|
228
|
+
this.addFinding({
|
|
229
|
+
title: 'Timelock Emergency Bypass',
|
|
230
|
+
description: `Function '${this.currentFunction}' appears to bypass timelock. If access control is compromised, attacker can execute malicious transactions immediately.`,
|
|
231
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
232
|
+
line: node.loc?.start?.line || 0,
|
|
233
|
+
code: funcCode.substring(0, 200),
|
|
234
|
+
severity: 'HIGH',
|
|
235
|
+
confidence: 'MEDIUM',
|
|
236
|
+
exploitable: true,
|
|
237
|
+
exploitabilityScore: 70,
|
|
238
|
+
attackVector: 'timelock-bypass',
|
|
239
|
+
recommendation: 'Emergency functions should still have minimum delay or require multi-sig. Document and audit all bypass mechanisms.'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Zero delay timelock
|
|
244
|
+
if (/setDelay|updateDelay/i.test(funcName)) {
|
|
245
|
+
if (!/require.*delay\s*>=|minDelay|MIN_DELAY/i.test(funcCode)) {
|
|
246
|
+
this.addFinding({
|
|
247
|
+
title: 'Timelock Delay Can Be Set to Zero',
|
|
248
|
+
description: `Function '${this.currentFunction}' may allow setting timelock delay to zero, effectively disabling governance protection.`,
|
|
249
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
250
|
+
line: node.loc?.start?.line || 0,
|
|
251
|
+
code: funcCode.substring(0, 200),
|
|
252
|
+
severity: 'CRITICAL',
|
|
253
|
+
confidence: 'MEDIUM',
|
|
254
|
+
exploitable: true,
|
|
255
|
+
exploitabilityScore: 85,
|
|
256
|
+
attackVector: 'timelock-disable',
|
|
257
|
+
recommendation: 'Enforce minimum delay: require(newDelay >= MIN_DELAY) where MIN_DELAY is at least 24-48 hours.'
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Detect flash loan governance patterns across contract
|
|
265
|
+
*/
|
|
266
|
+
detectFlashLoanGovernance(funcCode, node, funcName) {
|
|
267
|
+
// Check for token transfer functions that could enable flash loan attacks
|
|
268
|
+
if (/transfer|transferFrom/i.test(funcCode) && /vote|governance/i.test(this.currentContract.toLowerCase())) {
|
|
269
|
+
// Check if voting checkpoints are updated on transfer
|
|
270
|
+
const updatesCheckpoint = /_writeCheckpoint|_moveVotingPower|_afterTokenTransfer/i.test(funcCode);
|
|
271
|
+
|
|
272
|
+
if (!updatesCheckpoint && /balanceOf/.test(funcCode)) {
|
|
273
|
+
this.addFinding({
|
|
274
|
+
title: 'Voting Power Not Checkpointed on Transfer',
|
|
275
|
+
description: `Token transfer in governance contract doesn't checkpoint voting power. This enables flash loan attacks where attacker borrows tokens, votes, and returns in same block.`,
|
|
276
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
277
|
+
line: node.loc?.start?.line || 0,
|
|
278
|
+
code: funcCode.substring(0, 200),
|
|
279
|
+
severity: 'CRITICAL',
|
|
280
|
+
confidence: 'MEDIUM',
|
|
281
|
+
exploitable: true,
|
|
282
|
+
exploitabilityScore: 85,
|
|
283
|
+
attackVector: 'flash-loan-governance',
|
|
284
|
+
recommendation: 'Use ERC20Votes or implement checkpointing in _afterTokenTransfer to track historical balances.'
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
analyzeGovernancePatterns() {
|
|
291
|
+
// Cross-reference governance patterns for compound vulnerabilities
|
|
292
|
+
if (this.governancePatterns.length > 0) {
|
|
293
|
+
// Check for lack of quorum
|
|
294
|
+
const hasQuorum = this.sourceCode.match(/quorum|minVotes|minimumVotes/i);
|
|
295
|
+
if (!hasQuorum) {
|
|
296
|
+
const govContract = this.governancePatterns[0];
|
|
297
|
+
this.addFinding({
|
|
298
|
+
title: 'Missing Quorum Requirement',
|
|
299
|
+
description: `Governance contract '${govContract.contract}' may lack quorum requirements. Proposals could pass with minimal participation, enabling governance capture with small token holdings.`,
|
|
300
|
+
location: `Contract: ${govContract.contract}`,
|
|
301
|
+
line: govContract.node.loc?.start?.line || 0,
|
|
302
|
+
severity: 'HIGH',
|
|
303
|
+
confidence: 'MEDIUM',
|
|
304
|
+
exploitable: true,
|
|
305
|
+
exploitabilityScore: 70,
|
|
306
|
+
attackVector: 'low-quorum-governance',
|
|
307
|
+
recommendation: 'Implement quorum requirement: require(forVotes + againstVotes >= quorum())'
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
generateFlashLoanGovernancePoC() {
|
|
314
|
+
return `// SPDX-License-Identifier: MIT
|
|
315
|
+
pragma solidity ^0.8.0;
|
|
316
|
+
|
|
317
|
+
import "forge-std/Test.sol";
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Flash Loan Governance Attack PoC
|
|
321
|
+
* Exploits voting based on current balance without snapshot
|
|
322
|
+
*/
|
|
323
|
+
interface IGovernance {
|
|
324
|
+
function propose(address[] calldata targets, uint256[] calldata values, bytes[] calldata calldatas, string calldata description) external returns (uint256);
|
|
325
|
+
function castVote(uint256 proposalId, uint8 support) external;
|
|
326
|
+
function execute(uint256 proposalId) external;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface IFlashLoan {
|
|
330
|
+
function flashLoan(address token, uint256 amount, bytes calldata data) external;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
contract GovernanceExploit is Test {
|
|
334
|
+
IGovernance governance;
|
|
335
|
+
IFlashLoan flashLender;
|
|
336
|
+
address govToken;
|
|
337
|
+
|
|
338
|
+
function testFlashLoanGovernanceAttack() public {
|
|
339
|
+
// 1. Create malicious proposal (drain treasury)
|
|
340
|
+
address[] memory targets = new address[](1);
|
|
341
|
+
uint256[] memory values = new uint256[](1);
|
|
342
|
+
bytes[] memory calldatas = new bytes[](1);
|
|
343
|
+
|
|
344
|
+
targets[0] = address(governance);
|
|
345
|
+
calldatas[0] = abi.encodeWithSignature("withdrawAll(address)", address(this));
|
|
346
|
+
|
|
347
|
+
uint256 proposalId = governance.propose(targets, values, calldatas, "Drain treasury");
|
|
348
|
+
|
|
349
|
+
// 2. Flash loan massive amount of governance tokens
|
|
350
|
+
// flashLender.flashLoan(govToken, 10_000_000e18, abi.encode(proposalId));
|
|
351
|
+
|
|
352
|
+
// In callback:
|
|
353
|
+
// - Cast vote with flash loaned tokens
|
|
354
|
+
// governance.castVote(proposalId, 1); // Vote yes
|
|
355
|
+
// - Return tokens to flash lender
|
|
356
|
+
|
|
357
|
+
// 3. Execute proposal (if no timelock)
|
|
358
|
+
// governance.execute(proposalId);
|
|
359
|
+
|
|
360
|
+
// Result: Treasury drained with zero capital
|
|
361
|
+
}
|
|
362
|
+
}`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = GovernanceAttackDetector;
|