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,446 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Storage Collision Detector for Proxy Contracts
5
+ * Detects storage layout conflicts between proxy and implementation contracts
6
+ *
7
+ * Attack vectors detected:
8
+ * 1. Unstructured storage collision - Implementation overwrites proxy admin slot
9
+ * 2. Inherited storage mismatch - Proxy and impl have different inheritance
10
+ * 3. Missing storage gap - Upgradeable contracts without __gap
11
+ * 4. Non-upgradeable base - Using non-upgradeable OZ contracts
12
+ * 5. Function selector collision - Proxy and impl have same selector
13
+ */
14
+ class StorageCollisionDetector extends BaseDetector {
15
+ constructor() {
16
+ super(
17
+ 'Storage Collision',
18
+ 'Detects proxy storage collisions and upgrade safety issues',
19
+ 'CRITICAL'
20
+ );
21
+ this.currentContract = null;
22
+ this.isProxy = false;
23
+ this.isUpgradeable = false;
24
+ this.stateVariables = [];
25
+ this.inheritedContracts = [];
26
+ this.proxyPatterns = [];
27
+ this.storageSlots = new Map();
28
+
29
+ // Known proxy admin slots (EIP-1967)
30
+ this.KNOWN_SLOTS = {
31
+ IMPLEMENTATION: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc',
32
+ ADMIN: '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103',
33
+ BEACON: '0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50',
34
+ };
35
+ }
36
+
37
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
38
+ this.findings = [];
39
+ this.ast = ast;
40
+ this.sourceCode = sourceCode;
41
+ this.fileName = fileName;
42
+ this.sourceLines = sourceCode.split('\n');
43
+ this.cfg = cfg;
44
+ this.dataFlow = dataFlow;
45
+ this.stateVariables = [];
46
+ this.inheritedContracts = [];
47
+ this.proxyPatterns = [];
48
+
49
+ this.traverse(ast);
50
+
51
+ // Analyze if this is a proxy or upgradeable contract
52
+ this.analyzeProxyPatterns();
53
+
54
+ return this.findings;
55
+ }
56
+
57
+ visitContractDefinition(node) {
58
+ this.currentContract = node.name;
59
+ this.stateVariables = [];
60
+ this.inheritedContracts = [];
61
+
62
+ // Check if this is a proxy contract
63
+ const contractCode = this.getCodeSnippet(node.loc);
64
+ this.isProxy = this.detectProxyPattern(contractCode, node);
65
+ this.isUpgradeable = this.detectUpgradeablePattern(contractCode, node);
66
+
67
+ // Track inherited contracts
68
+ if (node.baseContracts) {
69
+ node.baseContracts.forEach(base => {
70
+ if (base.baseName) {
71
+ this.inheritedContracts.push({
72
+ name: base.baseName.namePath || base.baseName.name,
73
+ loc: base.loc
74
+ });
75
+ }
76
+ });
77
+ }
78
+ }
79
+
80
+ visitStateVariableDeclaration(node) {
81
+ if (!node.variables) return;
82
+
83
+ node.variables.forEach((variable, index) => {
84
+ this.stateVariables.push({
85
+ name: variable.name,
86
+ type: this.getTypeName(variable.typeName),
87
+ visibility: variable.visibility,
88
+ isConstant: variable.isDeclaredConst,
89
+ isImmutable: variable.isImmutable,
90
+ loc: variable.loc,
91
+ slot: this.calculateStorageSlot(variable, index)
92
+ });
93
+ });
94
+ }
95
+
96
+ visitFunctionDefinition(node) {
97
+ if (!this.isProxy && !this.isUpgradeable) return;
98
+
99
+ const funcName = node.name || '';
100
+ const funcCode = node.body ? this.getCodeSnippet(node.loc) : '';
101
+
102
+ // Check for delegatecall in proxy
103
+ if (funcCode.includes('delegatecall')) {
104
+ this.proxyPatterns.push({
105
+ type: 'delegatecall',
106
+ function: funcName,
107
+ loc: node.loc
108
+ });
109
+ }
110
+
111
+ // Check for assembly storage access
112
+ if (funcCode.includes('sstore') || funcCode.includes('sload')) {
113
+ this.analyzeAssemblyStorageAccess(funcCode, node);
114
+ }
115
+
116
+ // Check for initializer patterns
117
+ if (/initialize|init/i.test(funcName)) {
118
+ this.analyzeInitializer(node, funcCode);
119
+ }
120
+ }
121
+
122
+ detectProxyPattern(code, node) {
123
+ const proxyIndicators = [
124
+ /Proxy|proxy/,
125
+ /delegatecall/,
126
+ /implementation/i,
127
+ /ERC1967|EIP1967/i,
128
+ /TransparentUpgradeable/i,
129
+ /UUPSUpgradeable/i,
130
+ /BeaconProxy/i,
131
+ ];
132
+
133
+ return proxyIndicators.some(p => p.test(code));
134
+ }
135
+
136
+ detectUpgradeablePattern(code, node) {
137
+ const upgradeableIndicators = [
138
+ /Upgradeable/i,
139
+ /Initializable/i,
140
+ /UUPSUpgradeable/i,
141
+ /__gap/,
142
+ /initializer\s+modifier/i,
143
+ ];
144
+
145
+ return upgradeableIndicators.some(p => p.test(code));
146
+ }
147
+
148
+ analyzeAssemblyStorageAccess(funcCode, node) {
149
+ // Extract slots used in assembly
150
+ const slotPatterns = [
151
+ /sstore\s*\(\s*([x0-9a-fA-F]+)/g,
152
+ /sload\s*\(\s*([x0-9a-fA-F]+)/g,
153
+ /\.slot/g,
154
+ ];
155
+
156
+ for (const pattern of slotPatterns) {
157
+ let match;
158
+ while ((match = pattern.exec(funcCode)) !== null) {
159
+ const slot = match[1];
160
+ if (this.isKnownProxySlot(slot)) {
161
+ // Safe - using known proxy slot
162
+ } else if (this.couldCollide(slot)) {
163
+ this.reportPotentialSlotCollision(node, slot);
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ analyzeInitializer(node, funcCode) {
170
+ const funcName = node.name || 'initialize';
171
+
172
+ // Check for initializer modifier
173
+ const hasInitializerMod = node.modifiers &&
174
+ node.modifiers.some(m => /initializer|onlyInitializing/i.test(m.name));
175
+
176
+ // Check for reinitializer
177
+ const hasReinitializer = node.modifiers &&
178
+ node.modifiers.some(m => /reinitializer/i.test(m.name));
179
+
180
+ if (!hasInitializerMod && !hasReinitializer) {
181
+ this.addFinding({
182
+ title: 'Missing Initializer Modifier',
183
+ description: `Function '${funcName}' appears to be an initializer but lacks the 'initializer' modifier. Without this modifier, the function can be called multiple times, potentially allowing re-initialization attacks.`,
184
+ location: `Contract: ${this.currentContract}, Function: ${funcName}`,
185
+ line: node.loc?.start?.line || 0,
186
+ column: node.loc?.start?.column || 0,
187
+ code: funcCode.substring(0, 200),
188
+ severity: 'CRITICAL',
189
+ confidence: 'HIGH',
190
+ exploitable: true,
191
+ exploitabilityScore: 90,
192
+ attackVector: 're-initialization',
193
+ recommendation: `Add the initializer modifier from OpenZeppelin:
194
+ function initialize(...) external initializer {
195
+ __Ownable_init();
196
+ // ... rest of initialization
197
+ }
198
+
199
+ For version upgrades, use reinitializer(version):
200
+ function initializeV2(...) external reinitializer(2) { ... }`,
201
+ references: [
202
+ 'https://docs.openzeppelin.com/contracts/4.x/api/proxy#Initializable'
203
+ ]
204
+ });
205
+ }
206
+
207
+ // Check for _disableInitializers in constructor
208
+ if (!this.sourceCode.includes('_disableInitializers')) {
209
+ this.addFinding({
210
+ title: 'Missing _disableInitializers in Constructor',
211
+ description: `Upgradeable contract '${this.currentContract}' does not call _disableInitializers() in constructor. This allows an attacker to initialize the implementation contract directly, potentially causing issues with the proxy.`,
212
+ location: `Contract: ${this.currentContract}`,
213
+ line: 1,
214
+ column: 0,
215
+ code: '',
216
+ severity: 'HIGH',
217
+ confidence: 'MEDIUM',
218
+ exploitable: true,
219
+ exploitabilityScore: 70,
220
+ attackVector: 'implementation-initialization',
221
+ recommendation: `Add constructor that disables initializers:
222
+ constructor() {
223
+ _disableInitializers();
224
+ }
225
+
226
+ This prevents the implementation contract from being initialized directly.`
227
+ });
228
+ }
229
+ }
230
+
231
+ analyzeProxyPatterns() {
232
+ if (!this.isProxy && !this.isUpgradeable) return;
233
+
234
+ // Check for storage gap in upgradeable contracts
235
+ if (this.isUpgradeable && !this.hasStorageGap()) {
236
+ this.reportMissingStorageGap();
237
+ }
238
+
239
+ // Check for non-upgradeable base contracts
240
+ this.checkNonUpgradeableBases();
241
+
242
+ // Check for storage collision risks
243
+ this.checkStorageCollisionRisks();
244
+
245
+ // Check for function selector collision
246
+ this.checkSelectorCollision();
247
+ }
248
+
249
+ hasStorageGap() {
250
+ return this.stateVariables.some(v =>
251
+ v.name === '__gap' || v.name.includes('__gap')
252
+ ) || this.sourceCode.includes('__gap');
253
+ }
254
+
255
+ checkNonUpgradeableBases() {
256
+ const nonUpgradeableOZ = [
257
+ 'Ownable', 'ERC20', 'ERC721', 'ERC1155', 'ReentrancyGuard',
258
+ 'Pausable', 'AccessControl', 'ERC20Permit'
259
+ ];
260
+
261
+ for (const base of this.inheritedContracts) {
262
+ // Check if using non-upgradeable version
263
+ if (nonUpgradeableOZ.some(nuo => base.name === nuo)) {
264
+ this.addFinding({
265
+ title: 'Non-Upgradeable Base Contract in Upgradeable Contract',
266
+ description: `Contract '${this.currentContract}' inherits from '${base.name}' which is not upgradeable-safe. This can cause storage collisions when upgrading.
267
+
268
+ The non-upgradeable version uses a constructor which doesn't work with proxies, and may have different storage layout than the upgradeable version.`,
269
+ location: `Contract: ${this.currentContract}, Base: ${base.name}`,
270
+ line: base.loc?.start?.line || 0,
271
+ column: base.loc?.start?.column || 0,
272
+ code: `inherits ${base.name}`,
273
+ severity: 'CRITICAL',
274
+ confidence: 'HIGH',
275
+ exploitable: true,
276
+ exploitabilityScore: 85,
277
+ attackVector: 'storage-collision',
278
+ recommendation: `Use the upgradeable version:
279
+ - Ownable → OwnableUpgradeable
280
+ - ERC20 → ERC20Upgradeable
281
+ - ERC721 → ERC721Upgradeable
282
+ - ReentrancyGuard → ReentrancyGuardUpgradeable
283
+ - Pausable → PausableUpgradeable
284
+ - AccessControl → AccessControlUpgradeable
285
+
286
+ And call __ContractName_init() in your initializer.`,
287
+ references: [
288
+ 'https://docs.openzeppelin.com/contracts/4.x/upgradeable'
289
+ ]
290
+ });
291
+ }
292
+ }
293
+ }
294
+
295
+ checkStorageCollisionRisks() {
296
+ // Check if first state variable could collide with proxy slots
297
+ if (this.stateVariables.length > 0) {
298
+ const firstVar = this.stateVariables[0];
299
+
300
+ // Slot 0 collision with some proxy patterns
301
+ if (!firstVar.isConstant && !firstVar.isImmutable) {
302
+ // Check if this contract is used as implementation
303
+ if (this.isUpgradeable) {
304
+ // Ensure first variable doesn't collide with proxy admin
305
+ // Most modern proxies use EIP-1967 slots, but older patterns used slot 0
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ checkSelectorCollision() {
312
+ if (!this.cfg) return;
313
+
314
+ const selectors = new Map();
315
+
316
+ for (const [funcKey, funcInfo] of this.cfg.functions) {
317
+ if (funcInfo.visibility !== 'public' && funcInfo.visibility !== 'external') {
318
+ continue;
319
+ }
320
+
321
+ const selector = this.calculateSelector(funcInfo.name, funcInfo.parameters);
322
+ if (selector) {
323
+ if (selectors.has(selector)) {
324
+ // Selector collision
325
+ this.addFinding({
326
+ title: 'Function Selector Collision',
327
+ description: `Functions '${selectors.get(selector)}' and '${funcInfo.name}' have the same selector. In a proxy pattern, this can cause unexpected behavior as calls to one function may be routed to another.`,
328
+ location: `Contract: ${this.currentContract}`,
329
+ line: funcInfo.node?.loc?.start?.line || 0,
330
+ column: 0,
331
+ code: '',
332
+ severity: 'HIGH',
333
+ confidence: 'HIGH',
334
+ exploitable: true,
335
+ exploitabilityScore: 75,
336
+ attackVector: 'selector-collision',
337
+ recommendation: `Rename one of the functions to avoid selector collision. You can use tools like 'cast sig' to check selectors:
338
+ cast sig "functionName(type1,type2)"`
339
+ });
340
+ }
341
+ selectors.set(selector, funcInfo.name);
342
+ }
343
+ }
344
+ }
345
+
346
+ reportMissingStorageGap() {
347
+ this.addFinding({
348
+ title: 'Missing Storage Gap in Upgradeable Contract',
349
+ description: `Contract '${this.currentContract}' is upgradeable but doesn't include a __gap storage variable. Without a gap, adding new state variables in future upgrades will shift storage layout, corrupting existing data.`,
350
+ location: `Contract: ${this.currentContract}`,
351
+ line: 1,
352
+ column: 0,
353
+ code: this.sourceLines.slice(0, 10).join('\n'),
354
+ severity: 'HIGH',
355
+ confidence: 'HIGH',
356
+ exploitable: false,
357
+ exploitabilityScore: 30,
358
+ attackVector: 'upgrade-storage-collision',
359
+ recommendation: `Add a storage gap at the end of your state variables:
360
+ uint256[50] private __gap;
361
+
362
+ This reserves 50 storage slots for future upgrades. When adding new variables, reduce the gap size accordingly.
363
+
364
+ Example:
365
+ // V1
366
+ uint256 public value1;
367
+ uint256[49] private __gap; // 49 slots reserved
368
+
369
+ // V2 - adding value2
370
+ uint256 public value1;
371
+ uint256 public value2; // New variable
372
+ uint256[48] private __gap; // Reduced to 48`,
373
+ references: [
374
+ 'https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps'
375
+ ]
376
+ });
377
+ }
378
+
379
+ reportPotentialSlotCollision(node, slot) {
380
+ this.addFinding({
381
+ title: 'Potential Storage Slot Collision',
382
+ description: `Assembly code accesses storage slot ${slot} which may collide with inherited contract storage or proxy admin slots. Manual slot access bypasses Solidity's storage layout, risking data corruption.`,
383
+ location: `Contract: ${this.currentContract}, Function: ${node.name || 'unknown'}`,
384
+ line: node.loc?.start?.line || 0,
385
+ column: node.loc?.start?.column || 0,
386
+ code: this.getCodeSnippet(node.loc)?.substring(0, 200) || '',
387
+ severity: 'HIGH',
388
+ confidence: 'MEDIUM',
389
+ exploitable: true,
390
+ exploitabilityScore: 60,
391
+ attackVector: 'storage-collision',
392
+ recommendation: `Use EIP-1967 standard slots for proxy storage:
393
+ bytes32 constant IMPLEMENTATION_SLOT = 0x360894...;
394
+ bytes32 constant ADMIN_SLOT = 0xb53127...;
395
+
396
+ Or use namespaced storage (ERC-7201):
397
+ bytes32 constant STORAGE_LOCATION = keccak256("myprotocol.storage.main");`
398
+ });
399
+ }
400
+
401
+ // Helper methods
402
+
403
+ calculateStorageSlot(variable, index) {
404
+ // Simplified - actual slot calculation is complex
405
+ if (variable.isDeclaredConst || variable.isImmutable) {
406
+ return null; // No storage slot
407
+ }
408
+ return index; // Simplified sequential slots
409
+ }
410
+
411
+ isKnownProxySlot(slot) {
412
+ const normalizedSlot = slot.toLowerCase();
413
+ return Object.values(this.KNOWN_SLOTS).some(known =>
414
+ known.toLowerCase() === normalizedSlot
415
+ );
416
+ }
417
+
418
+ couldCollide(slot) {
419
+ // Check if slot could collide with regular storage (low slots)
420
+ try {
421
+ const slotNum = BigInt(slot);
422
+ return slotNum < 100n; // Low slots more likely to collide
423
+ } catch {
424
+ return false;
425
+ }
426
+ }
427
+
428
+ calculateSelector(funcName, parameters) {
429
+ if (!funcName) return null;
430
+ // Simplified selector calculation
431
+ // Real implementation would use keccak256
432
+ const signature = `${funcName}(${parameters.map(p => p.type).join(',')})`;
433
+ return signature; // Would be first 4 bytes of keccak256
434
+ }
435
+
436
+ getTypeName(typeName) {
437
+ if (!typeName) return 'unknown';
438
+ if (typeName.type === 'ElementaryTypeName') return typeName.name;
439
+ if (typeName.type === 'UserDefinedTypeName') return typeName.namePath;
440
+ if (typeName.type === 'ArrayTypeName') return `${this.getTypeName(typeName.baseTypeName)}[]`;
441
+ if (typeName.type === 'Mapping') return 'mapping';
442
+ return 'complex';
443
+ }
444
+ }
445
+
446
+ module.exports = StorageCollisionDetector;