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,362 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Callback Reentrancy Detector (ERC777, ERC1155, ERC721)
5
+ *
6
+ * Detects reentrancy via token callback hooks - a critical attack vector
7
+ * for high-TVL DeFi protocols that was exploited in:
8
+ * - Uniswap/Lendf.Me ($25M, 2020)
9
+ * - Cream Finance ($130M, 2021)
10
+ * - Fei Protocol ($80M, 2022)
11
+ *
12
+ * Attack vectors:
13
+ * 1. ERC777 tokensToSend/tokensReceived hooks
14
+ * 2. ERC1155 onERC1155Received callbacks
15
+ * 3. ERC721 onERC721Received callbacks
16
+ * 4. Flash loan callbacks (onFlashLoan)
17
+ *
18
+ * Immunefi Critical: Direct theft via callback-triggered reentrancy
19
+ */
20
+ class CallbackReentrancyDetector extends BaseDetector {
21
+ constructor() {
22
+ super(
23
+ 'Callback Reentrancy',
24
+ 'Detects reentrancy via ERC777/ERC1155/ERC721/FlashLoan callbacks',
25
+ 'CRITICAL'
26
+ );
27
+ this.currentContract = null;
28
+ this.currentFunction = null;
29
+ this.tokenInteractions = [];
30
+ this.stateChanges = [];
31
+ this.externalCalls = [];
32
+ }
33
+
34
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
35
+ this.findings = [];
36
+ this.ast = ast;
37
+ this.sourceCode = sourceCode;
38
+ this.fileName = fileName;
39
+ this.sourceLines = sourceCode.split('\n');
40
+ this.cfg = cfg;
41
+ this.dataFlow = dataFlow;
42
+ this.tokenInteractions = [];
43
+ this.stateChanges = [];
44
+ this.externalCalls = [];
45
+
46
+ this.traverse(ast);
47
+ this.analyzeCallbackVulnerabilities();
48
+
49
+ return this.findings;
50
+ }
51
+
52
+ visitContractDefinition(node) {
53
+ this.currentContract = node.name;
54
+
55
+ // Check if contract handles tokens that have callbacks
56
+ const baseContracts = (node.baseContracts || [])
57
+ .map(b => b.baseName?.namePath || '')
58
+ .join(' ');
59
+
60
+ // Detect if this is a callback-receiving contract
61
+ this.isCallbackReceiver = /IERC1155Receiver|ERC1155Holder|IERC721Receiver|ERC721Holder|IERC777Recipient|IERC777Sender/i.test(baseContracts);
62
+ }
63
+
64
+ visitFunctionDefinition(node) {
65
+ this.currentFunction = node.name || 'constructor';
66
+ const funcCode = this.getCodeSnippet(node.loc);
67
+
68
+ // Skip pure/view functions (can't have reentrancy with state changes)
69
+ if (node.stateMutability === 'pure' || node.stateMutability === 'view') {
70
+ return;
71
+ }
72
+
73
+ // Skip internal/private functions (analyze public entry points)
74
+ if (node.visibility === 'private' || node.visibility === 'internal') {
75
+ return;
76
+ }
77
+
78
+ // Detect token transfer patterns that trigger callbacks
79
+ this.detectCallbackTriggers(funcCode, node);
80
+
81
+ // Detect if function is itself a callback handler
82
+ this.detectCallbackHandlers(funcCode, node);
83
+ }
84
+
85
+ /**
86
+ * Detect functions that make token transfers which could trigger callbacks
87
+ */
88
+ detectCallbackTriggers(funcCode, node) {
89
+ const funcName = this.currentFunction;
90
+
91
+ // ERC777 transfer triggers tokensToSend/tokensReceived hooks
92
+ const erc777Patterns = [
93
+ { pattern: /\.send\s*\([^)]*\)/i, token: 'ERC777', callback: 'tokensReceived' },
94
+ { pattern: /\.transfer\s*\([^)]*\)/i, token: 'ERC777/ERC20', callback: 'tokensReceived (if ERC777)' },
95
+ { pattern: /\.transferFrom\s*\([^)]*\)/i, token: 'ERC777/ERC20', callback: 'tokensReceived (if ERC777)' },
96
+ { pattern: /\.operatorSend\s*\([^)]*\)/i, token: 'ERC777', callback: 'tokensReceived' },
97
+ ];
98
+
99
+ // ERC1155 safe transfers trigger onERC1155Received
100
+ const erc1155Patterns = [
101
+ { pattern: /\.safeTransferFrom\s*\([^)]*\)/i, token: 'ERC1155/ERC721', callback: 'onERC1155Received/onERC721Received' },
102
+ { pattern: /\.safeBatchTransferFrom\s*\([^)]*\)/i, token: 'ERC1155', callback: 'onERC1155BatchReceived' },
103
+ { pattern: /\._safeTransfer\s*\([^)]*\)/i, token: 'ERC1155/ERC721', callback: 'onERC1155Received/onERC721Received' },
104
+ { pattern: /\._safeMint\s*\([^)]*\)/i, token: 'ERC721/ERC1155', callback: 'onERC721Received/onERC1155Received' },
105
+ ];
106
+
107
+ // Flash loan callbacks
108
+ const flashLoanPatterns = [
109
+ { pattern: /\.flashLoan\s*\([^)]*\)/i, token: 'FlashLoan', callback: 'onFlashLoan/executeOperation' },
110
+ { pattern: /\.flash\s*\([^)]*\)/i, token: 'Uniswap V3', callback: 'uniswapV3FlashCallback' },
111
+ { pattern: /\.swap\s*\([^)]*\)/i, token: 'Uniswap V2/V3', callback: 'uniswapV2Call/uniswapV3SwapCallback' },
112
+ ];
113
+
114
+ const allPatterns = [...erc777Patterns, ...erc1155Patterns, ...flashLoanPatterns];
115
+
116
+ for (const { pattern, token, callback } of allPatterns) {
117
+ if (pattern.test(funcCode)) {
118
+ // Check if there's state modification after the call
119
+ const hasStateChangeAfter = this.detectStateChangeAfterCall(funcCode, pattern);
120
+ const hasReentrancyGuard = /nonReentrant|_status|_locked|ReentrancyGuard/i.test(funcCode);
121
+
122
+ if (hasStateChangeAfter && !hasReentrancyGuard) {
123
+ this.tokenInteractions.push({
124
+ function: funcName,
125
+ tokenType: token,
126
+ callback: callback,
127
+ line: node.loc?.start?.line || 0,
128
+ code: funcCode,
129
+ node: node
130
+ });
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Detect if this function is a callback handler that could be exploited
138
+ */
139
+ detectCallbackHandlers(funcCode, node) {
140
+ const funcName = this.currentFunction;
141
+
142
+ // Known callback function names
143
+ const callbackNames = [
144
+ 'onERC1155Received',
145
+ 'onERC1155BatchReceived',
146
+ 'onERC721Received',
147
+ 'tokensReceived',
148
+ 'tokensToSend',
149
+ 'onFlashLoan',
150
+ 'executeOperation', // Aave flash loan
151
+ 'uniswapV2Call',
152
+ 'uniswapV3FlashCallback',
153
+ 'uniswapV3SwapCallback',
154
+ 'pancakeCall',
155
+ 'BiswapCall',
156
+ 'onTokenTransfer', // Chainlink
157
+ 'receiveFlashLoan', // Balancer
158
+ ];
159
+
160
+ if (callbackNames.some(cb => funcName.toLowerCase() === cb.toLowerCase())) {
161
+ // This is a callback handler - check for dangerous operations
162
+ const hasDangerousOps = this.detectDangerousCallbackOperations(funcCode);
163
+
164
+ if (hasDangerousOps.length > 0) {
165
+ this.addFinding({
166
+ title: 'Dangerous Callback Handler',
167
+ description: `Callback handler '${funcName}' performs dangerous operations: ${hasDangerousOps.join(', ')}.\n\n` +
168
+ `Attack scenario:\n` +
169
+ `1. Attacker deploys malicious contract that inherits callback interface\n` +
170
+ `2. Attacker triggers token operation that calls back to their contract\n` +
171
+ `3. Callback re-enters vulnerable function before state is finalized\n` +
172
+ `4. Attacker extracts value due to stale state`,
173
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
174
+ line: node.loc?.start?.line || 0,
175
+ column: node.loc?.start?.column || 0,
176
+ code: funcCode.substring(0, 400),
177
+ severity: 'CRITICAL',
178
+ confidence: 'HIGH',
179
+ exploitable: true,
180
+ exploitabilityScore: 90,
181
+ attackVector: 'callback-reentrancy',
182
+ recommendation: `1. Add nonReentrant modifier to callback handlers\n` +
183
+ `2. Follow checks-effects-interactions pattern\n` +
184
+ `3. Update state BEFORE making external calls\n` +
185
+ `4. Consider using pull-payment pattern instead of push`,
186
+ references: [
187
+ 'https://eips.ethereum.org/EIPS/eip-777',
188
+ 'https://eips.ethereum.org/EIPS/eip-1155',
189
+ 'https://blog.openzeppelin.com/reentrancy-after-istanbul'
190
+ ],
191
+ foundryPoC: this.generateCallbackPoC(funcName)
192
+ });
193
+ }
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Detect dangerous operations within callback handlers
199
+ */
200
+ detectDangerousCallbackOperations(funcCode) {
201
+ const dangerous = [];
202
+
203
+ // External calls
204
+ if (/\.call\s*\{|\.call\s*\(|\.delegatecall\s*\(|\.transfer\s*\(|\.send\s*\(/i.test(funcCode)) {
205
+ dangerous.push('external calls');
206
+ }
207
+
208
+ // State modifications (balance changes, mapping updates)
209
+ if (/balances\s*\[|_balances\s*\[|shares\s*\[|deposits\s*\[/i.test(funcCode)) {
210
+ dangerous.push('balance state changes');
211
+ }
212
+
213
+ // Token minting/burning
214
+ if (/_mint\s*\(|_burn\s*\(/i.test(funcCode)) {
215
+ dangerous.push('token minting/burning');
216
+ }
217
+
218
+ // Value extraction
219
+ if (/withdraw|redeem|claim|harvest/i.test(funcCode)) {
220
+ dangerous.push('value extraction');
221
+ }
222
+
223
+ return dangerous;
224
+ }
225
+
226
+ /**
227
+ * Detect if state changes occur after a callback-triggering call
228
+ */
229
+ detectStateChangeAfterCall(funcCode, callPattern) {
230
+ const match = funcCode.match(callPattern);
231
+ if (!match) return false;
232
+
233
+ const afterCall = funcCode.substring(match.index + match[0].length);
234
+
235
+ // Look for state-changing patterns after the call
236
+ const stateChangePatterns = [
237
+ /\w+\s*\[.*\]\s*[+\-*/]?=/, // mapping[key] = value
238
+ /\w+\s*=\s*[^=]/, // variable = value (not comparison)
239
+ /\+\+\w+|\w+\+\+|--\w+|\w+--/, // increment/decrement
240
+ /_mint\s*\(|_burn\s*\(/, // token operations
241
+ /\.push\s*\(|\.pop\s*\(/, // array operations
242
+ /delete\s+\w+/, // deletion
243
+ ];
244
+
245
+ return stateChangePatterns.some(p => p.test(afterCall));
246
+ }
247
+
248
+ /**
249
+ * Analyze collected interactions for callback vulnerabilities
250
+ */
251
+ analyzeCallbackVulnerabilities() {
252
+ for (const interaction of this.tokenInteractions) {
253
+ // Check CFG for reentrancy guard
254
+ let hasGuard = false;
255
+ if (this.cfg?.functions) {
256
+ const funcKey = `${this.currentContract}.${interaction.function}`;
257
+ const funcInfo = this.cfg.functions.get(funcKey);
258
+ if (funcInfo?.modifiers?.some(m =>
259
+ /nonreentrant|reentrancyguard|locked/i.test(m.name || ''))) {
260
+ hasGuard = true;
261
+ }
262
+ }
263
+
264
+ if (!hasGuard) {
265
+ this.addFinding({
266
+ title: `${interaction.tokenType} Callback Reentrancy`,
267
+ description: `Function '${interaction.function}' makes ${interaction.tokenType} transfer that triggers '${interaction.callback}' callback, ` +
268
+ `then modifies state. An attacker can exploit this by:\n\n` +
269
+ `1. Deploying contract that implements ${interaction.callback}\n` +
270
+ `2. In callback, re-enter ${interaction.function}\n` +
271
+ `3. Exploit stale state before original call completes\n\n` +
272
+ `This is the attack pattern used in:\n` +
273
+ `- Uniswap/Lendf.Me ($25M exploit)\n` +
274
+ `- Cream Finance ($130M exploit)\n` +
275
+ `- Multiple DeFi protocol incidents`,
276
+ location: `Contract: ${this.currentContract}, Function: ${interaction.function}`,
277
+ line: interaction.line,
278
+ code: interaction.code.substring(0, 400),
279
+ severity: 'CRITICAL',
280
+ confidence: 'HIGH',
281
+ exploitable: true,
282
+ exploitabilityScore: 95,
283
+ attackVector: 'callback-reentrancy',
284
+ recommendation: `1. Add ReentrancyGuard (nonReentrant modifier)\n` +
285
+ `2. Update ALL state BEFORE token transfers\n` +
286
+ `3. Consider using OpenZeppelin's ReentrancyGuard\n` +
287
+ `4. For flash loans, validate loan amount matches expected`,
288
+ references: [
289
+ 'https://swcregistry.io/docs/SWC-107',
290
+ 'https://blog.openzeppelin.com/reentrancy-after-istanbul'
291
+ ],
292
+ foundryPoC: this.generateCallbackPoC(interaction.function, interaction.tokenType)
293
+ });
294
+ }
295
+ }
296
+ }
297
+
298
+ generateCallbackPoC(funcName, tokenType = 'ERC1155') {
299
+ return `// SPDX-License-Identifier: MIT
300
+ pragma solidity ^0.8.0;
301
+
302
+ import "forge-std/Test.sol";
303
+
304
+ /**
305
+ * PoC: ${tokenType} Callback Reentrancy Exploit
306
+ * Target: ${funcName}
307
+ *
308
+ * Attack: Re-enter via token callback before state update
309
+ */
310
+ interface IVictim {
311
+ function ${funcName}(address, uint256, bytes calldata) external;
312
+ }
313
+
314
+ contract CallbackAttacker is Test {
315
+ IVictim victim;
316
+ uint256 public attackCount;
317
+
318
+ function setUp() public {
319
+ // victim = IVictim(VICTIM_ADDRESS);
320
+ }
321
+
322
+ // ERC1155 callback - triggers on safeTransferFrom
323
+ function onERC1155Received(
324
+ address operator,
325
+ address from,
326
+ uint256 id,
327
+ uint256 value,
328
+ bytes calldata data
329
+ ) external returns (bytes4) {
330
+ attackCount++;
331
+ if (attackCount < 5) {
332
+ // Re-enter victim contract
333
+ // victim.${funcName}(...);
334
+ }
335
+ return this.onERC1155Received.selector;
336
+ }
337
+
338
+ // ERC777 callback
339
+ function tokensReceived(
340
+ address operator,
341
+ address from,
342
+ address to,
343
+ uint256 amount,
344
+ bytes calldata userData,
345
+ bytes calldata operatorData
346
+ ) external {
347
+ attackCount++;
348
+ if (attackCount < 5) {
349
+ // Re-enter victim
350
+ }
351
+ }
352
+
353
+ function testExploit() public {
354
+ // 1. Trigger ${funcName} with attacker as recipient
355
+ // 2. Callback re-enters before state finalized
356
+ // 3. Extract value multiple times
357
+ }
358
+ }`;
359
+ }
360
+ }
361
+
362
+ module.exports = CallbackReentrancyDetector;