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.
Files changed (42) hide show
  1. package/README.md +685 -0
  2. package/bin/web3crit +10 -0
  3. package/package.json +59 -0
  4. package/src/analyzers/control-flow.js +256 -0
  5. package/src/analyzers/data-flow.js +720 -0
  6. package/src/analyzers/exploit-chain.js +751 -0
  7. package/src/analyzers/immunefi-classifier.js +515 -0
  8. package/src/analyzers/poc-validator.js +396 -0
  9. package/src/analyzers/solodit-enricher.js +1122 -0
  10. package/src/cli.js +546 -0
  11. package/src/detectors/access-control-enhanced.js +458 -0
  12. package/src/detectors/base-detector.js +213 -0
  13. package/src/detectors/callback-reentrancy.js +362 -0
  14. package/src/detectors/cross-contract-reentrancy.js +697 -0
  15. package/src/detectors/delegatecall.js +167 -0
  16. package/src/detectors/deprecated-functions.js +62 -0
  17. package/src/detectors/flash-loan.js +408 -0
  18. package/src/detectors/frontrunning.js +553 -0
  19. package/src/detectors/gas-griefing.js +701 -0
  20. package/src/detectors/governance-attacks.js +366 -0
  21. package/src/detectors/integer-overflow.js +487 -0
  22. package/src/detectors/oracle-manipulation.js +524 -0
  23. package/src/detectors/permit-exploits.js +368 -0
  24. package/src/detectors/precision-loss.js +408 -0
  25. package/src/detectors/price-manipulation-advanced.js +548 -0
  26. package/src/detectors/proxy-vulnerabilities.js +651 -0
  27. package/src/detectors/readonly-reentrancy.js +473 -0
  28. package/src/detectors/rebasing-token-vault.js +416 -0
  29. package/src/detectors/reentrancy-enhanced.js +359 -0
  30. package/src/detectors/selfdestruct.js +259 -0
  31. package/src/detectors/share-manipulation.js +412 -0
  32. package/src/detectors/signature-replay.js +409 -0
  33. package/src/detectors/storage-collision.js +446 -0
  34. package/src/detectors/timestamp-dependence.js +494 -0
  35. package/src/detectors/toctou.js +427 -0
  36. package/src/detectors/token-standard-compliance.js +465 -0
  37. package/src/detectors/unchecked-call.js +214 -0
  38. package/src/detectors/vault-inflation.js +421 -0
  39. package/src/index.js +42 -0
  40. package/src/package-lock.json +2874 -0
  41. package/src/package.json +39 -0
  42. 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;