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,473 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Read-Only Reentrancy Detector
5
+ * Detects vulnerabilities where view functions return stale data during reentrancy
6
+ *
7
+ * This is the attack vector used in the Curve Finance exploit and similar attacks.
8
+ * View functions may return incorrect values when called during the execution of
9
+ * another function that modifies state but hasn't completed yet.
10
+ *
11
+ * Attack vectors detected:
12
+ * 1. View functions reading state that's modified by non-view functions with external calls
13
+ * 2. Price/rate calculations that can be manipulated via reentrancy
14
+ * 3. Virtual price functions in LP tokens
15
+ * 4. Share/asset calculations during mint/burn
16
+ */
17
+ class ReadOnlyReentrancyDetector extends BaseDetector {
18
+ constructor() {
19
+ super(
20
+ 'Read-Only Reentrancy',
21
+ 'Detects view functions vulnerable to read-only reentrancy attacks',
22
+ 'CRITICAL'
23
+ );
24
+ this.currentContract = null;
25
+ this.viewFunctions = new Map();
26
+ this.stateModifyingFunctions = new Map();
27
+ this.externalCallFunctions = [];
28
+ this.sharedStateAccess = new Map();
29
+ }
30
+
31
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
32
+ this.findings = [];
33
+ this.ast = ast;
34
+ this.sourceCode = sourceCode;
35
+ this.fileName = fileName;
36
+ this.sourceLines = sourceCode.split('\n');
37
+ this.cfg = cfg;
38
+ this.dataFlow = dataFlow;
39
+ this.viewFunctions.clear();
40
+ this.stateModifyingFunctions.clear();
41
+ this.externalCallFunctions = [];
42
+ this.sharedStateAccess.clear();
43
+
44
+ if (!cfg) {
45
+ return this.findings;
46
+ }
47
+
48
+ // First pass: categorize functions
49
+ this.categorizeFunctions();
50
+
51
+ // Second pass: find cross-function vulnerabilities
52
+ this.findReadOnlyReentrancy();
53
+
54
+ // Third pass: check for specific vulnerable patterns
55
+ this.checkVulnerablePatterns();
56
+
57
+ return this.findings;
58
+ }
59
+
60
+ categorizeFunctions() {
61
+ for (const [funcKey, funcInfo] of this.cfg.functions) {
62
+ const funcCode = funcInfo.node?.body ? this.getCodeSnippet(funcInfo.node.loc) : '';
63
+
64
+ // Categorize view/pure functions
65
+ if (funcInfo.stateMutability === 'view' || funcInfo.stateMutability === 'pure') {
66
+ this.viewFunctions.set(funcKey, {
67
+ info: funcInfo,
68
+ code: funcCode,
69
+ readsState: funcInfo.stateReads.map(r => r.variable),
70
+ isPriceFunction: this.isPriceRelatedFunction(funcInfo.name, funcCode)
71
+ });
72
+ }
73
+
74
+ // Categorize state-modifying functions with external calls
75
+ if (funcInfo.externalCalls.length > 0 && funcInfo.stateWrites.length > 0) {
76
+ this.stateModifyingFunctions.set(funcKey, {
77
+ info: funcInfo,
78
+ code: funcCode,
79
+ externalCalls: funcInfo.externalCalls,
80
+ writesState: funcInfo.stateWrites.map(w => w.variable),
81
+ hasReentrancyGuard: this.hasReentrancyGuard(funcInfo)
82
+ });
83
+
84
+ this.externalCallFunctions.push({
85
+ key: funcKey,
86
+ info: funcInfo,
87
+ code: funcCode
88
+ });
89
+ }
90
+ }
91
+ }
92
+
93
+ findReadOnlyReentrancy() {
94
+ // For each state-modifying function with external calls
95
+ for (const [modFuncKey, modFunc] of this.stateModifyingFunctions) {
96
+ // Skip if has reentrancy guard (but note: guard doesn't help view functions!)
97
+ // Actually, regular reentrancy guards DO NOT prevent read-only reentrancy
98
+
99
+ // Find view functions that read state this function writes
100
+ for (const [viewFuncKey, viewFunc] of this.viewFunctions) {
101
+ // Check for shared state variables
102
+ const sharedState = modFunc.writesState.filter(writeVar =>
103
+ viewFunc.readsState.some(readVar =>
104
+ readVar === writeVar || this.variablesOverlap(readVar, writeVar)
105
+ )
106
+ );
107
+
108
+ if (sharedState.length > 0) {
109
+ // Check if there's a window for exploitation
110
+ const vulnerability = this.analyzeReentrancyWindow(modFunc, viewFunc, sharedState);
111
+
112
+ if (vulnerability.isVulnerable) {
113
+ this.reportReadOnlyReentrancy(modFunc, viewFunc, sharedState, vulnerability);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ analyzeReentrancyWindow(modFunc, viewFunc, sharedState) {
121
+ // Check if external call happens BEFORE state update (classic pattern)
122
+ // OR if state is partially updated when external call happens
123
+
124
+ const funcCode = modFunc.code;
125
+
126
+ // Find positions of external calls and state writes
127
+ let externalCallLine = 0;
128
+ let lastStateWriteLine = 0;
129
+
130
+ for (const call of modFunc.info.externalCalls) {
131
+ if (call.loc?.start?.line > externalCallLine) {
132
+ externalCallLine = call.loc.start.line;
133
+ }
134
+ }
135
+
136
+ for (const write of modFunc.info.stateWrites) {
137
+ if (write.loc?.start?.line > lastStateWriteLine) {
138
+ lastStateWriteLine = write.loc.start.line;
139
+ }
140
+ }
141
+
142
+ // Vulnerable if: external call happens before last state write
143
+ // OR if view function reads derived value (like price) that depends on multiple state vars
144
+ const callBeforeWrite = externalCallLine < lastStateWriteLine;
145
+ const viewReadsDerivedValue = viewFunc.isPriceFunction;
146
+
147
+ return {
148
+ isVulnerable: callBeforeWrite || viewReadsDerivedValue,
149
+ callBeforeWrite,
150
+ viewReadsDerivedValue,
151
+ externalCallLine,
152
+ lastStateWriteLine
153
+ };
154
+ }
155
+
156
+ checkVulnerablePatterns() {
157
+ // Check for specific known vulnerable patterns
158
+
159
+ // Pattern 1: Curve-style virtual price in LP tokens
160
+ this.checkCurveVirtualPrice();
161
+
162
+ // Pattern 2: ERC4626-style share price
163
+ this.checkSharePriceVulnerability();
164
+
165
+ // Pattern 3: Balancer-style rate providers
166
+ this.checkRateProviderVulnerability();
167
+
168
+ // Pattern 4: Lending protocol exchange rates
169
+ this.checkExchangeRateVulnerability();
170
+ }
171
+
172
+ checkCurveVirtualPrice() {
173
+ // Check for get_virtual_price or similar patterns
174
+ const curvePatterns = [
175
+ /get_virtual_price/i,
176
+ /virtualPrice/i,
177
+ /getVirtualPrice/i,
178
+ ];
179
+
180
+ for (const [funcKey, viewFunc] of this.viewFunctions) {
181
+ const funcName = viewFunc.info.name || '';
182
+ const funcCode = viewFunc.code;
183
+
184
+ if (curvePatterns.some(p => p.test(funcName) || p.test(funcCode))) {
185
+ // Check if there are functions that modify the underlying state
186
+ const hasVulnerableModifier = this.hasStateModifierWithCallback(viewFunc.readsState);
187
+
188
+ if (hasVulnerableModifier) {
189
+ this.addFinding({
190
+ title: 'Curve-Style Virtual Price Read-Only Reentrancy',
191
+ description: `Function '${funcName}' returns a virtual price that can be manipulated during reentrancy. An attacker can:
192
+ 1. Call a function that modifies reserves/balances with a callback (e.g., remove_liquidity with ETH)
193
+ 2. During the callback, call this view function which returns stale price
194
+ 3. Use the manipulated price in another protocol (lending, DEX, etc.)
195
+
196
+ This attack vector was used in the Curve Finance exploit causing >$50M in losses.`,
197
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
198
+ line: viewFunc.info.node?.loc?.start?.line || 0,
199
+ column: 0,
200
+ code: funcCode.substring(0, 200),
201
+ severity: 'CRITICAL',
202
+ confidence: 'HIGH',
203
+ exploitable: true,
204
+ exploitabilityScore: 95,
205
+ attackVector: 'read-only-reentrancy',
206
+ recommendation: `Implement reentrancy lock that also blocks view functions:
207
+ 1. Use a reentrancy guard that reverts in view functions
208
+ 2. Or use Vyper's @nonreentrant decorator which protects all functions
209
+ 3. For integrators: Don't trust virtual_price during callbacks
210
+
211
+ Example view function protection:
212
+ modifier nonReentrantView() {
213
+ require(!_locked, "Reentrancy");
214
+ _;
215
+ }`,
216
+ references: [
217
+ 'https://chainsecurity.com/curve-lp-oracle-manipulation-post-mortem/',
218
+ 'https://blog.openzeppelin.com/read-only-reentrancy-is-real'
219
+ ],
220
+ foundryPoC: this.generateReadOnlyReentrancyPoC(funcName)
221
+ });
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ checkSharePriceVulnerability() {
228
+ // ERC4626 and similar share-based systems
229
+ const sharePricePatterns = [
230
+ /convertToAssets/i,
231
+ /convertToShares/i,
232
+ /pricePerShare/i,
233
+ /sharePrice/i,
234
+ /exchangeRate/i,
235
+ /getRate/i,
236
+ ];
237
+
238
+ for (const [funcKey, viewFunc] of this.viewFunctions) {
239
+ const funcName = viewFunc.info.name || '';
240
+
241
+ if (sharePricePatterns.some(p => p.test(funcName))) {
242
+ // Check if mint/burn functions have external calls
243
+ const hasMintBurnWithCallback = this.hasMintBurnWithCallback();
244
+
245
+ if (hasMintBurnWithCallback) {
246
+ this.addFinding({
247
+ title: 'Share Price Vulnerable to Read-Only Reentrancy',
248
+ description: `Function '${funcName}' calculates share/asset conversion which may return incorrect values during mint/burn operations that include callbacks (e.g., safeTransferFrom with callback).
249
+
250
+ During these callbacks, totalSupply or totalAssets may be in an intermediate state, allowing manipulation of the apparent share price.`,
251
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
252
+ line: viewFunc.info.node?.loc?.start?.line || 0,
253
+ column: 0,
254
+ code: viewFunc.code.substring(0, 200),
255
+ severity: 'HIGH',
256
+ confidence: 'MEDIUM',
257
+ exploitable: true,
258
+ exploitabilityScore: 70,
259
+ attackVector: 'read-only-reentrancy',
260
+ recommendation: `1. Ensure state updates complete before any external calls (CEI pattern)
261
+ 2. Use reentrancy guards that also protect view functions
262
+ 3. Consider caching prices at the start of transactions`
263
+ });
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ checkRateProviderVulnerability() {
270
+ // Balancer-style rate providers
271
+ const ratePatterns = [
272
+ /getRate\s*\(/i,
273
+ /IRateProvider/i,
274
+ /rateProvider/i,
275
+ ];
276
+
277
+ if (ratePatterns.some(p => p.test(this.sourceCode))) {
278
+ // Check for implementation
279
+ for (const [funcKey, viewFunc] of this.viewFunctions) {
280
+ if (/getRate/i.test(viewFunc.info.name || '')) {
281
+ this.addFinding({
282
+ title: 'Rate Provider Potential Read-Only Reentrancy',
283
+ description: `Contract implements rate provider pattern (getRate). If the underlying rate calculation depends on state that can be manipulated during callbacks, this creates a read-only reentrancy vector.
284
+
285
+ Protocols integrating this rate provider may use stale rates during their operations.`,
286
+ location: `Contract: ${this.currentContract}, Function: ${viewFunc.info.name}`,
287
+ line: viewFunc.info.node?.loc?.start?.line || 0,
288
+ column: 0,
289
+ code: viewFunc.code.substring(0, 200),
290
+ severity: 'MEDIUM',
291
+ confidence: 'MEDIUM',
292
+ exploitable: true,
293
+ exploitabilityScore: 55,
294
+ attackVector: 'read-only-reentrancy',
295
+ recommendation: `Ensure rate calculations are not affected by reentrancy:
296
+ 1. Cache rate at start of state-modifying functions
297
+ 2. Use Balancer's rate provider update mechanism
298
+ 3. Document reentrancy behavior for integrators`
299
+ });
300
+ }
301
+ }
302
+ }
303
+ }
304
+
305
+ checkExchangeRateVulnerability() {
306
+ // Compound-style exchange rates
307
+ const exchangePatterns = [
308
+ /exchangeRateStored/i,
309
+ /exchangeRateCurrent/i,
310
+ /underlying.*balance|balance.*underlying/i,
311
+ ];
312
+
313
+ for (const [funcKey, viewFunc] of this.viewFunctions) {
314
+ const funcName = viewFunc.info.name || '';
315
+ const funcCode = viewFunc.code;
316
+
317
+ if (exchangePatterns.some(p => p.test(funcName) || p.test(funcCode))) {
318
+ if (this.hasStateModifierWithCallback(viewFunc.readsState)) {
319
+ this.addFinding({
320
+ title: 'Exchange Rate Read-Only Reentrancy Risk',
321
+ description: `Function '${funcName}' calculates exchange rate which may be vulnerable during reentrant calls. Lending protocols and other integrators may receive manipulated rates.`,
322
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
323
+ line: viewFunc.info.node?.loc?.start?.line || 0,
324
+ column: 0,
325
+ code: funcCode.substring(0, 200),
326
+ severity: 'HIGH',
327
+ confidence: 'MEDIUM',
328
+ exploitable: true,
329
+ exploitabilityScore: 65,
330
+ attackVector: 'read-only-reentrancy',
331
+ recommendation: `Use non-reentrant exchange rate calculation or document the risk for integrators.`
332
+ });
333
+ }
334
+ }
335
+ }
336
+ }
337
+
338
+ // Helper methods
339
+
340
+ isPriceRelatedFunction(funcName, funcCode) {
341
+ if (!funcName) return false;
342
+
343
+ const pricePatterns = [
344
+ /price/i, /rate/i, /value/i, /convert/i, /exchange/i,
345
+ /virtual/i, /balance/i, /supply/i, /reserves/i
346
+ ];
347
+
348
+ return pricePatterns.some(p => p.test(funcName) || p.test(funcCode));
349
+ }
350
+
351
+ hasReentrancyGuard(funcInfo) {
352
+ if (!funcInfo.modifiers) return false;
353
+
354
+ const guardPatterns = [
355
+ /nonReentrant/i, /lock/i, /mutex/i, /noReentrancy/i
356
+ ];
357
+
358
+ return funcInfo.modifiers.some(mod =>
359
+ guardPatterns.some(p => p.test(mod))
360
+ );
361
+ }
362
+
363
+ variablesOverlap(var1, var2) {
364
+ // Check if variables might refer to same storage
365
+ // e.g., "balances" and "balances[msg.sender]"
366
+ const base1 = var1.split('[')[0];
367
+ const base2 = var2.split('[')[0];
368
+ return base1 === base2;
369
+ }
370
+
371
+ hasStateModifierWithCallback(stateVars) {
372
+ // Check if any function modifies these state vars and has external calls
373
+ for (const [funcKey, modFunc] of this.stateModifyingFunctions) {
374
+ const modifiesSharedState = stateVars.some(sv =>
375
+ modFunc.writesState.some(ws => this.variablesOverlap(sv, ws))
376
+ );
377
+
378
+ if (modifiesSharedState && modFunc.externalCalls.length > 0) {
379
+ return true;
380
+ }
381
+ }
382
+ return false;
383
+ }
384
+
385
+ hasMintBurnWithCallback() {
386
+ // Check for mint/burn functions with potential callbacks
387
+ for (const func of this.externalCallFunctions) {
388
+ const funcName = (func.info.name || '').toLowerCase();
389
+ if (/mint|burn|deposit|withdraw|redeem/i.test(funcName)) {
390
+ // Check if external call could be a callback (ERC20/721 hooks, etc.)
391
+ const hasCallback = func.code.includes('safeTransfer') ||
392
+ func.code.includes('_beforeTokenTransfer') ||
393
+ func.code.includes('_afterTokenTransfer') ||
394
+ func.code.includes('.call');
395
+ if (hasCallback) return true;
396
+ }
397
+ }
398
+ return false;
399
+ }
400
+
401
+ reportReadOnlyReentrancy(modFunc, viewFunc, sharedState, vulnerability) {
402
+ const modFuncName = modFunc.info.name || 'unknown';
403
+ const viewFuncName = viewFunc.info.name || 'unknown';
404
+
405
+ this.addFinding({
406
+ title: 'Read-Only Reentrancy Vulnerability',
407
+ description: `View function '${viewFuncName}' reads state (${sharedState.join(', ')}) that is modified by '${modFuncName}' which has external calls.
408
+
409
+ During the external call in '${modFuncName}', an attacker can call '${viewFuncName}' and receive stale data. This can be exploited if other protocols use this view function for price/rate calculations.
410
+
411
+ ${vulnerability.callBeforeWrite ? 'External call happens BEFORE state update - classic reentrancy window.' : ''}
412
+ ${vulnerability.viewReadsDerivedValue ? 'View function calculates derived value (price/rate) - high impact if manipulated.' : ''}`,
413
+ location: `Contract: ${this.currentContract}`,
414
+ line: viewFunc.info.node?.loc?.start?.line || 0,
415
+ column: 0,
416
+ code: viewFunc.code.substring(0, 200),
417
+ severity: vulnerability.viewReadsDerivedValue ? 'CRITICAL' : 'HIGH',
418
+ confidence: 'HIGH',
419
+ exploitable: true,
420
+ exploitabilityScore: vulnerability.viewReadsDerivedValue ? 85 : 70,
421
+ attackVector: 'read-only-reentrancy',
422
+ recommendation: `1. Apply CEI (Checks-Effects-Interactions) pattern: update all state BEFORE external calls
423
+ 2. Use reentrancy guard that also reverts in view functions:
424
+ modifier nonReentrantView() {
425
+ require(!_locked, "ReentrancyGuard: reentrant call");
426
+ _;
427
+ }
428
+ 3. For price functions, consider caching price at transaction start
429
+ 4. Document for integrators that view functions may return stale data during callbacks`,
430
+ references: [
431
+ 'https://blog.openzeppelin.com/read-only-reentrancy-is-real',
432
+ 'https://chainsecurity.com/heartbreaks-curve-lp-oracles/'
433
+ ]
434
+ });
435
+ }
436
+
437
+ generateReadOnlyReentrancyPoC(funcName) {
438
+ return `// SPDX-License-Identifier: MIT
439
+ pragma solidity ^0.8.0;
440
+
441
+ import "forge-std/Test.sol";
442
+
443
+ /**
444
+ * Proof of Concept: Read-Only Reentrancy Attack
445
+ * Exploits stale ${funcName} during callback
446
+ */
447
+ contract ReadOnlyReentrancyExploit is Test {
448
+ // ITarget target;
449
+ // IIntegrator integrator; // Protocol that uses target's view function
450
+
451
+ function testExploit() public {
452
+ // Step 1: Call function that triggers callback (e.g., remove_liquidity)
453
+ // This will call our receive() or fallback() during execution
454
+ // target.remove_liquidity{value: 1 ether}(...);
455
+ }
456
+
457
+ receive() external payable {
458
+ // Step 2: During callback, the state is inconsistent
459
+ // View function returns manipulated value
460
+ // uint256 manipulatedPrice = target.${funcName}();
461
+
462
+ // Step 3: Use manipulated price in another protocol
463
+ // e.g., borrow against inflated collateral value
464
+ // integrator.borrow(manipulatedPrice * myCollateral / 1e18);
465
+
466
+ // Step 4: After this callback returns, target's state is updated
467
+ // but we already exploited the stale price
468
+ }
469
+ }`;
470
+ }
471
+ }
472
+
473
+ module.exports = ReadOnlyReentrancyDetector;