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,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;
|