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,651 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Proxy Contract Vulnerabilities Detector
|
|
5
|
+
* Detects critical issues in upgradeable contracts (UUPS, Transparent Proxies, EIP-1967)
|
|
6
|
+
*
|
|
7
|
+
* Detects:
|
|
8
|
+
* - Missing initialization protection (uninitialized proxy)
|
|
9
|
+
* - Storage collision in proxy patterns
|
|
10
|
+
* - Unauthorized upgrade functions
|
|
11
|
+
* - Missing implementation contract validation
|
|
12
|
+
* - UUPS-specific vulnerabilities (missing _authorizeUpgrade)
|
|
13
|
+
* - Transparent proxy selector clash
|
|
14
|
+
* - Storage layout incompatibility warnings
|
|
15
|
+
*/
|
|
16
|
+
class ProxyVulnerabilitiesDetector extends BaseDetector {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(
|
|
19
|
+
'Proxy Contract Vulnerabilities',
|
|
20
|
+
'Detects critical vulnerabilities in upgradeable proxy contracts (UUPS, Transparent, EIP-1967)',
|
|
21
|
+
'CRITICAL'
|
|
22
|
+
);
|
|
23
|
+
this.currentContract = null;
|
|
24
|
+
this.proxyPatterns = {
|
|
25
|
+
uups: false,
|
|
26
|
+
transparent: false,
|
|
27
|
+
beacon: false,
|
|
28
|
+
diamond: false
|
|
29
|
+
};
|
|
30
|
+
this.hasInitializer = false;
|
|
31
|
+
this.hasInitialized = false;
|
|
32
|
+
this.upgradeFunctions = [];
|
|
33
|
+
this.storageSlots = new Set();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
37
|
+
this.findings = [];
|
|
38
|
+
this.ast = ast;
|
|
39
|
+
this.sourceCode = sourceCode;
|
|
40
|
+
this.fileName = fileName;
|
|
41
|
+
this.sourceLines = sourceCode.split('\n');
|
|
42
|
+
this.cfg = cfg;
|
|
43
|
+
this.dataFlow = dataFlow;
|
|
44
|
+
|
|
45
|
+
this.traverse(ast);
|
|
46
|
+
|
|
47
|
+
// Post-traversal analysis
|
|
48
|
+
this.analyzeProxyPatterns();
|
|
49
|
+
|
|
50
|
+
return this.findings;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
visitContractDefinition(node) {
|
|
54
|
+
this.currentContract = node.name;
|
|
55
|
+
this.proxyPatterns = {
|
|
56
|
+
uups: false,
|
|
57
|
+
transparent: false,
|
|
58
|
+
beacon: false,
|
|
59
|
+
diamond: false
|
|
60
|
+
};
|
|
61
|
+
this.hasInitializer = false;
|
|
62
|
+
this.hasInitialized = false;
|
|
63
|
+
this.upgradeFunctions = [];
|
|
64
|
+
this.storageSlots = new Set();
|
|
65
|
+
|
|
66
|
+
// Check if contract is a proxy implementation
|
|
67
|
+
const contractNameLower = (node.name || '').toLowerCase();
|
|
68
|
+
const isProxyContract = contractNameLower.includes('proxy') ||
|
|
69
|
+
contractNameLower.includes('upgradeable') ||
|
|
70
|
+
contractNameLower.includes('implementation');
|
|
71
|
+
|
|
72
|
+
if (node.baseContracts && node.baseContracts.length > 0) {
|
|
73
|
+
node.baseContracts.forEach(base => {
|
|
74
|
+
const baseName = base.baseName.namePath || base.baseName.name;
|
|
75
|
+
if (this.isProxyBaseContract(baseName)) {
|
|
76
|
+
this.detectProxyPattern(baseName);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Also check contract name for proxy patterns
|
|
82
|
+
if (isProxyContract) {
|
|
83
|
+
// Mark as potential proxy to enable upgrade function detection
|
|
84
|
+
this.proxyPatterns.transparent = true; // Default to transparent if name suggests proxy
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
visitFunctionDefinition(node) {
|
|
89
|
+
const funcName = node.name || '';
|
|
90
|
+
const funcCode = this.getCodeSnippet(node.loc);
|
|
91
|
+
const funcCodeLower = funcCode.toLowerCase();
|
|
92
|
+
|
|
93
|
+
// Detect initializer functions
|
|
94
|
+
if (funcName.toLowerCase().includes('initializ') ||
|
|
95
|
+
funcCodeLower.includes('initializer') ||
|
|
96
|
+
funcCodeLower.includes('__init')) {
|
|
97
|
+
this.hasInitializer = true;
|
|
98
|
+
|
|
99
|
+
// Check if initializer is protected
|
|
100
|
+
if (!this.hasInitializerProtection(funcCode, node)) {
|
|
101
|
+
this.addFinding({
|
|
102
|
+
title: 'Unprotected Initializer Function',
|
|
103
|
+
description: `Initializer function '${funcName}' lacks proper protection against multiple initialization. Proxy contracts must prevent re-initialization to avoid storage corruption.`,
|
|
104
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
105
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
106
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
107
|
+
code: this.getCodeSnippet(node.loc),
|
|
108
|
+
severity: 'CRITICAL',
|
|
109
|
+
confidence: 'HIGH',
|
|
110
|
+
exploitable: true,
|
|
111
|
+
exploitabilityScore: 95,
|
|
112
|
+
attackVector: 'proxy-initialization',
|
|
113
|
+
recommendation: 'Use OpenZeppelin\'s Initializable pattern with initializer modifier, or check an initialized storage variable. Never allow re-initialization.',
|
|
114
|
+
references: [
|
|
115
|
+
'https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable',
|
|
116
|
+
'https://swcregistry.io/docs/SWC-118',
|
|
117
|
+
'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/utils/Initializable.sol'
|
|
118
|
+
],
|
|
119
|
+
foundryPoC: this.generateInitializerPoC(this.currentContract, funcName)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Detect upgrade functions
|
|
125
|
+
if (this.isUpgradeFunction(funcName, funcCode)) {
|
|
126
|
+
this.upgradeFunctions.push({
|
|
127
|
+
name: funcName,
|
|
128
|
+
node: node,
|
|
129
|
+
code: funcCode
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Check for unauthorized upgrades
|
|
133
|
+
if (!this.hasUpgradeProtection(funcCode, node)) {
|
|
134
|
+
this.addFinding({
|
|
135
|
+
title: 'Unauthorized Upgrade Function',
|
|
136
|
+
description: `Upgrade function '${funcName}' lacks access control. Anyone can upgrade the proxy implementation, potentially stealing funds or breaking functionality.`,
|
|
137
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
138
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
139
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
140
|
+
code: this.getCodeSnippet(node.loc),
|
|
141
|
+
severity: 'CRITICAL',
|
|
142
|
+
confidence: 'HIGH',
|
|
143
|
+
exploitable: true,
|
|
144
|
+
exploitabilityScore: 100,
|
|
145
|
+
attackVector: 'unauthorized-upgrade',
|
|
146
|
+
recommendation: 'Add access control (onlyOwner, onlyRole, etc.) to all upgrade functions. Consider using OpenZeppelin\'s UUPSUpgradeable or TransparentUpgradeableProxy.',
|
|
147
|
+
references: [
|
|
148
|
+
'https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable',
|
|
149
|
+
'https://swcregistry.io/docs/SWC-119'
|
|
150
|
+
],
|
|
151
|
+
foundryPoC: this.generateUpgradePoC(this.currentContract, funcName)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// UUPS-specific: Check for _authorizeUpgrade
|
|
157
|
+
if (this.proxyPatterns.uups) {
|
|
158
|
+
if (funcName === '_authorizeUpgrade' || funcCodeLower.includes('_authorizeupgrade')) {
|
|
159
|
+
// Good - UUPS pattern requires this
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
visitVariableDeclaration(node) {
|
|
166
|
+
// Track storage slots for collision detection
|
|
167
|
+
if (node.stateVariable && node.name) {
|
|
168
|
+
// Check for EIP-1967 storage slots
|
|
169
|
+
const code = this.getLineContent(node.loc ? node.loc.start.line : 0);
|
|
170
|
+
if (code.includes('bytes32') && code.includes('0x')) {
|
|
171
|
+
// Potential storage slot declaration
|
|
172
|
+
const slotMatch = code.match(/0x[0-9a-fA-F]{64}/);
|
|
173
|
+
if (slotMatch) {
|
|
174
|
+
this.storageSlots.add(slotMatch[0]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
visitMemberAccess(node) {
|
|
181
|
+
// Detect delegatecall usage (common in proxies)
|
|
182
|
+
if (node.memberName === 'delegatecall') {
|
|
183
|
+
// Get the full function context, not just the member access
|
|
184
|
+
const lineNum = node.loc ? node.loc.start.line : 0;
|
|
185
|
+
// Look for the containing function - get more context
|
|
186
|
+
let funcCode = '';
|
|
187
|
+
|
|
188
|
+
// Try to get more context - check surrounding lines for fallback/receive
|
|
189
|
+
let foundFallback = false;
|
|
190
|
+
for (let i = Math.max(1, lineNum - 30); i <= Math.min(this.sourceLines.length, lineNum + 10); i++) {
|
|
191
|
+
const line = this.sourceLines[i - 1] || '';
|
|
192
|
+
if (line.includes('fallback') || line.includes('receive')) {
|
|
193
|
+
foundFallback = true;
|
|
194
|
+
// This is in a fallback/receive function - get the full function
|
|
195
|
+
const funcStart = Math.max(1, i - 2);
|
|
196
|
+
const funcEnd = Math.min(this.sourceLines.length, i + 40);
|
|
197
|
+
funcCode = this.sourceLines.slice(funcStart - 1, funcEnd).join('\n');
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// If we didn't find fallback, get context around the delegatecall
|
|
203
|
+
if (!funcCode) {
|
|
204
|
+
const start = Math.max(0, lineNum - 15);
|
|
205
|
+
const end = Math.min(this.sourceLines.length, lineNum + 5);
|
|
206
|
+
funcCode = this.sourceLines.slice(start, end).join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!this.hasDelegatecallProtection(funcCode)) {
|
|
210
|
+
this.addFinding({
|
|
211
|
+
title: 'Unprotected Delegatecall in Proxy',
|
|
212
|
+
description: 'Delegatecall detected without proper validation of target address. Malicious implementation contracts can corrupt proxy storage.',
|
|
213
|
+
location: `Contract: ${this.currentContract}`,
|
|
214
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
215
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
216
|
+
code: this.getCodeSnippet(node.loc),
|
|
217
|
+
severity: 'CRITICAL',
|
|
218
|
+
confidence: 'MEDIUM',
|
|
219
|
+
exploitable: true,
|
|
220
|
+
exploitabilityScore: 90,
|
|
221
|
+
attackVector: 'delegatecall-exploit',
|
|
222
|
+
recommendation: 'Validate implementation address before delegatecall. Use OpenZeppelin\'s proxy patterns or ensure implementation is from trusted source.',
|
|
223
|
+
references: [
|
|
224
|
+
'https://swcregistry.io/docs/SWC-112',
|
|
225
|
+
'https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable'
|
|
226
|
+
]
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Detect storage slot access (EIP-1967)
|
|
232
|
+
if (node.memberName === 'sload' || node.memberName === 'sstore') {
|
|
233
|
+
// Check for EIP-1967 storage slots
|
|
234
|
+
const code = this.getCodeSnippet(node.loc);
|
|
235
|
+
const eip1967Slots = [
|
|
236
|
+
'0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', // IMPLEMENTATION_SLOT
|
|
237
|
+
'0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103', // ADMIN_SLOT
|
|
238
|
+
'0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7' // BEACON_SLOT
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
eip1967Slots.forEach(slot => {
|
|
242
|
+
if (code.includes(slot)) {
|
|
243
|
+
// This is using EIP-1967 - check for proper usage
|
|
244
|
+
if (!this.hasEIP1967Protection(code)) {
|
|
245
|
+
this.addFinding({
|
|
246
|
+
title: 'Unprotected EIP-1967 Storage Access',
|
|
247
|
+
description: `Direct access to EIP-1967 storage slot detected without proper access control. This could allow unauthorized upgrades or storage corruption.`,
|
|
248
|
+
location: `Contract: ${this.currentContract}`,
|
|
249
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
250
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
251
|
+
code: this.getCodeSnippet(node.loc),
|
|
252
|
+
severity: 'HIGH',
|
|
253
|
+
confidence: 'MEDIUM',
|
|
254
|
+
exploitable: true,
|
|
255
|
+
exploitabilityScore: 75,
|
|
256
|
+
attackVector: 'storage-slot-manipulation',
|
|
257
|
+
recommendation: 'Use OpenZeppelin\'s ERC1967Upgrade or similar library for safe storage slot access. Never directly manipulate EIP-1967 slots without access control.',
|
|
258
|
+
references: [
|
|
259
|
+
'https://eips.ethereum.org/EIPS/eip-1967',
|
|
260
|
+
'https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable'
|
|
261
|
+
]
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Analyze proxy patterns after traversal
|
|
271
|
+
*/
|
|
272
|
+
analyzeProxyPatterns() {
|
|
273
|
+
// UUPS pattern requires _authorizeUpgrade
|
|
274
|
+
if (this.proxyPatterns.uups) {
|
|
275
|
+
const hasAuthorizeUpgrade = this.sourceCode.toLowerCase().includes('_authorizeupgrade');
|
|
276
|
+
if (!hasAuthorizeUpgrade) {
|
|
277
|
+
this.addFinding({
|
|
278
|
+
title: 'Missing _authorizeUpgrade in UUPS Pattern',
|
|
279
|
+
description: 'Contract appears to use UUPS pattern but lacks _authorizeUpgrade function. This is required for UUPS proxies to prevent unauthorized upgrades.',
|
|
280
|
+
location: `Contract: ${this.currentContract}`,
|
|
281
|
+
line: 1,
|
|
282
|
+
column: 0,
|
|
283
|
+
code: this.sourceCode.substring(0, 200),
|
|
284
|
+
severity: 'CRITICAL',
|
|
285
|
+
confidence: 'HIGH',
|
|
286
|
+
exploitable: true,
|
|
287
|
+
exploitabilityScore: 95,
|
|
288
|
+
attackVector: 'uups-missing-authorization',
|
|
289
|
+
recommendation: 'Implement _authorizeUpgrade function with proper access control. Use OpenZeppelin\'s UUPSUpgradeable contract.',
|
|
290
|
+
references: [
|
|
291
|
+
'https://docs.openzeppelin.com/contracts/4.x/api/proxy#UUPSUpgradeable',
|
|
292
|
+
'https://eips.ethereum.org/EIPS/eip-1822'
|
|
293
|
+
]
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check for storage collision risks
|
|
299
|
+
if (this.storageSlots.size > 0) {
|
|
300
|
+
// Warn about potential storage layout issues
|
|
301
|
+
const hasStorageLayout = this.sourceCode.includes('StorageLayout') ||
|
|
302
|
+
this.sourceCode.includes('storage gap');
|
|
303
|
+
if (!hasStorageLayout && this.proxyPatterns.uups) {
|
|
304
|
+
this.addFinding({
|
|
305
|
+
title: 'Potential Storage Layout Collision',
|
|
306
|
+
description: 'Proxy implementation may have storage layout conflicts. Upgrading to a new implementation with different storage layout can corrupt data.',
|
|
307
|
+
location: `Contract: ${this.currentContract}`,
|
|
308
|
+
line: 1,
|
|
309
|
+
column: 0,
|
|
310
|
+
code: this.sourceCode.substring(0, 200),
|
|
311
|
+
severity: 'HIGH',
|
|
312
|
+
confidence: 'MEDIUM',
|
|
313
|
+
exploitable: false,
|
|
314
|
+
exploitabilityScore: 50,
|
|
315
|
+
attackVector: 'storage-collision',
|
|
316
|
+
recommendation: 'Use storage gaps (e.g., uint256[50] __gap) in base contracts. Document storage layout. Use OpenZeppelin\'s storage layout validation tools.',
|
|
317
|
+
references: [
|
|
318
|
+
'https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#storage-gaps',
|
|
319
|
+
'https://swcregistry.io/docs/SWC-120'
|
|
320
|
+
]
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Check if contract inherits from proxy base contracts
|
|
328
|
+
*/
|
|
329
|
+
isProxyBaseContract(baseName) {
|
|
330
|
+
const proxyBases = [
|
|
331
|
+
'UUPSUpgradeable',
|
|
332
|
+
'TransparentUpgradeableProxy',
|
|
333
|
+
'ERC1967Proxy',
|
|
334
|
+
'BeaconProxy',
|
|
335
|
+
'Diamond',
|
|
336
|
+
'DiamondProxy',
|
|
337
|
+
'UpgradeableProxy',
|
|
338
|
+
'Proxy'
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
return proxyBases.some(base =>
|
|
342
|
+
baseName.toLowerCase().includes(base.toLowerCase())
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Detect which proxy pattern is being used
|
|
348
|
+
*/
|
|
349
|
+
detectProxyPattern(baseName) {
|
|
350
|
+
const nameLower = baseName.toLowerCase();
|
|
351
|
+
if (nameLower.includes('uups')) {
|
|
352
|
+
this.proxyPatterns.uups = true;
|
|
353
|
+
} else if (nameLower.includes('transparent')) {
|
|
354
|
+
this.proxyPatterns.transparent = true;
|
|
355
|
+
} else if (nameLower.includes('beacon')) {
|
|
356
|
+
this.proxyPatterns.beacon = true;
|
|
357
|
+
} else if (nameLower.includes('diamond')) {
|
|
358
|
+
this.proxyPatterns.diamond = true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Check if function is an upgrade function
|
|
364
|
+
*/
|
|
365
|
+
isUpgradeFunction(funcName, code) {
|
|
366
|
+
const nameLower = (funcName || '').toLowerCase();
|
|
367
|
+
const codeLower = code.toLowerCase();
|
|
368
|
+
|
|
369
|
+
// Skip fallback and receive functions - these are for delegation, not upgrades
|
|
370
|
+
if (nameLower === 'fallback' || nameLower === '' || nameLower === 'receive') {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Check for explicit upgrade function names
|
|
375
|
+
const upgradeKeywords = ['upgrade', 'upgradeto', 'upgradetoandcall', 'changeimplementation',
|
|
376
|
+
'setimplementation', 'updateimplementation'];
|
|
377
|
+
const hasUpgradeKeyword = upgradeKeywords.some(keyword =>
|
|
378
|
+
nameLower.includes(keyword)
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
// Check for implementation assignment pattern (not just usage)
|
|
382
|
+
// e.g., implementation = newImpl, _setImplementation(newImpl)
|
|
383
|
+
const hasImplementationWrite = /implementation\s*=|_setimplementation\s*\(|_upgrade\s*\(/i.test(code);
|
|
384
|
+
|
|
385
|
+
const isUpgrade = hasUpgradeKeyword || hasImplementationWrite;
|
|
386
|
+
|
|
387
|
+
// Only consider it an upgrade function if it's in a contract that looks like a proxy
|
|
388
|
+
if (isUpgrade) {
|
|
389
|
+
const contractNameLower = (this.currentContract || '').toLowerCase();
|
|
390
|
+
const isProxyContract = this.proxyPatterns.uups || this.proxyPatterns.transparent ||
|
|
391
|
+
this.proxyPatterns.beacon || this.proxyPatterns.diamond ||
|
|
392
|
+
contractNameLower.includes('proxy') ||
|
|
393
|
+
contractNameLower.includes('upgradeable') ||
|
|
394
|
+
contractNameLower.includes('implementation');
|
|
395
|
+
|
|
396
|
+
if (!isProxyContract) {
|
|
397
|
+
// Not a proxy contract, skip
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return isUpgrade;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Check if initializer has protection
|
|
407
|
+
*/
|
|
408
|
+
hasInitializerProtection(code, node) {
|
|
409
|
+
const codeLower = code.toLowerCase();
|
|
410
|
+
|
|
411
|
+
// Check for initializer modifier
|
|
412
|
+
if (node.modifiers && node.modifiers.some(m =>
|
|
413
|
+
m.name && m.name.toLowerCase().includes('initializer')
|
|
414
|
+
)) {
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Check for initialized variable check
|
|
419
|
+
if (codeLower.includes('initialized') &&
|
|
420
|
+
(codeLower.includes('require') || codeLower.includes('revert'))) {
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check for initializer() modifier usage
|
|
425
|
+
if (codeLower.includes('initializer()') || codeLower.includes('!initialized')) {
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Check if upgrade function has protection
|
|
434
|
+
*/
|
|
435
|
+
hasUpgradeProtection(code, node) {
|
|
436
|
+
const codeLower = code.toLowerCase();
|
|
437
|
+
|
|
438
|
+
// Check for access control modifiers in the function definition
|
|
439
|
+
const accessControlModifiers = ['onlyowner', 'onlyrole', 'onlyadmin', 'onlygovernance', 'onlyauthorized'];
|
|
440
|
+
if (node.modifiers && node.modifiers.length > 0) {
|
|
441
|
+
const hasAccessControl = node.modifiers.some(m => {
|
|
442
|
+
if (!m || !m.name) return false;
|
|
443
|
+
const modName = m.name.toLowerCase();
|
|
444
|
+
return accessControlModifiers.some(ac => modName.includes(ac));
|
|
445
|
+
});
|
|
446
|
+
if (hasAccessControl) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Also check the function signature in code for modifiers
|
|
452
|
+
// Pattern: function upgrade(...) public onlyAdmin
|
|
453
|
+
// Modifiers can be on the same line or next line after function declaration
|
|
454
|
+
const modifierPatterns = [
|
|
455
|
+
/\bfunction\s+\w+\s*\([^)]*\)\s+[^{]*(onlyOwner|onlyRole|onlyAdmin|onlyGovernance|onlyAuthorized)/i,
|
|
456
|
+
/\bfunction\s+\w+\s*\([^)]*\)\s+public\s+(onlyOwner|onlyRole|onlyAdmin|onlyGovernance|onlyAuthorized)/i,
|
|
457
|
+
/\bfunction\s+\w+\s*\([^)]*\)\s+external\s+(onlyOwner|onlyRole|onlyAdmin|onlyGovernance|onlyAuthorized)/i
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
if (modifierPatterns.some(pattern => pattern.test(code))) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Check if modifier appears near the function declaration (within first few lines)
|
|
465
|
+
const lines = code.split('\n');
|
|
466
|
+
let foundFunction = false;
|
|
467
|
+
for (let i = 0; i < Math.min(5, lines.length); i++) {
|
|
468
|
+
const line = lines[i];
|
|
469
|
+
if (line.includes('function') && line.includes('upgrade')) {
|
|
470
|
+
foundFunction = true;
|
|
471
|
+
}
|
|
472
|
+
if (foundFunction && /\b(onlyOwner|onlyRole|onlyAdmin|onlyGovernance|onlyAuthorized)\b/i.test(line)) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Check for require statements with access control in the function body
|
|
478
|
+
const accessControlPatterns = [
|
|
479
|
+
/require\s*\(\s*msg\.sender\s*==\s*owner/i,
|
|
480
|
+
/require\s*\(\s*msg\.sender\s*==\s*admin/i,
|
|
481
|
+
/require\s*\(\s*hasRole/i,
|
|
482
|
+
/require\s*\(\s*_authorizeUpgrade/i,
|
|
483
|
+
/\bonlyOwner\b/i,
|
|
484
|
+
/\bonlyRole\b/i,
|
|
485
|
+
/\bonlyAdmin\b/i
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
// Also check if the function code contains modifier usage
|
|
489
|
+
if (accessControlPatterns.some(pattern => pattern.test(code))) {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Check if there's a require with approvedImplementations or similar whitelist
|
|
494
|
+
if (codeLower.includes('require') &&
|
|
495
|
+
(codeLower.includes('approvedimplementations') ||
|
|
496
|
+
codeLower.includes('trustedimplementations') ||
|
|
497
|
+
codeLower.includes('whitelist'))) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Check if delegatecall has protection
|
|
506
|
+
*/
|
|
507
|
+
hasDelegatecallProtection(code) {
|
|
508
|
+
const codeLower = code.toLowerCase();
|
|
509
|
+
|
|
510
|
+
// Check if it's in a fallback function (standard proxy pattern)
|
|
511
|
+
// Fallback functions with delegatecall are typically secure proxy patterns
|
|
512
|
+
if (codeLower.includes('fallback') || codeLower.includes('receive')) {
|
|
513
|
+
// Check if implementation is validated
|
|
514
|
+
if (codeLower.includes('require') && (codeLower.includes('implementation') || codeLower.includes('impl'))) {
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
// Assembly delegatecall in fallback is standard proxy pattern
|
|
518
|
+
// Check if there's a require before the delegatecall
|
|
519
|
+
const lines = code.split('\n');
|
|
520
|
+
let foundRequire = false;
|
|
521
|
+
let foundDelegatecall = false;
|
|
522
|
+
for (const line of lines) {
|
|
523
|
+
const lineLower = line.toLowerCase();
|
|
524
|
+
if (lineLower.includes('require') && (lineLower.includes('implementation') || lineLower.includes('impl'))) {
|
|
525
|
+
foundRequire = true;
|
|
526
|
+
}
|
|
527
|
+
if (lineLower.includes('delegatecall')) {
|
|
528
|
+
foundDelegatecall = true;
|
|
529
|
+
// If we found delegatecall and there was a require before, it's protected
|
|
530
|
+
if (foundRequire) {
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Assembly delegatecall in fallback with implementation variable is standard pattern
|
|
536
|
+
if (codeLower.includes('assembly') && codeLower.includes('delegatecall') &&
|
|
537
|
+
(codeLower.includes('implementation') || codeLower.includes('impl'))) {
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Should validate implementation address
|
|
543
|
+
const validationPatterns = [
|
|
544
|
+
/require\s*\(\s*.*implementation/i,
|
|
545
|
+
/require\s*\(\s*.*impl\s*!=\s*address\(0\)/i,
|
|
546
|
+
/require\s*\(\s*.*code\.length\s*>\s*0/i,
|
|
547
|
+
/isContract\s*\(/i,
|
|
548
|
+
/address\(.*\)\.code\.length/i,
|
|
549
|
+
/approvedImplementations\[/i,
|
|
550
|
+
/trustedImplementations\[/i
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
return validationPatterns.some(pattern => pattern.test(codeLower));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Check if EIP-1967 storage access has protection
|
|
558
|
+
*/
|
|
559
|
+
hasEIP1967Protection(code) {
|
|
560
|
+
const codeLower = code.toLowerCase();
|
|
561
|
+
|
|
562
|
+
// Should have access control
|
|
563
|
+
return /onlyOwner|onlyRole|_authorizeUpgrade|require.*msg\.sender/i.test(codeLower);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Generate Foundry PoC for initializer exploit
|
|
568
|
+
*/
|
|
569
|
+
generateInitializerPoC(contractName, funcName) {
|
|
570
|
+
return `// SPDX-License-Identifier: MIT
|
|
571
|
+
pragma solidity ^0.8.0;
|
|
572
|
+
|
|
573
|
+
import "forge-std/Test.sol";
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Proof of Concept: Unprotected Initializer Exploit
|
|
577
|
+
* Target: ${contractName}.${funcName}()
|
|
578
|
+
* Attack Vector: Re-initialize proxy to corrupt storage
|
|
579
|
+
*/
|
|
580
|
+
contract InitializerExploit is Test {
|
|
581
|
+
address constant PROXY = address(0); // ${contractName} proxy address
|
|
582
|
+
address attacker = address(this);
|
|
583
|
+
|
|
584
|
+
function testExploit() public {
|
|
585
|
+
// 1. Call initializer multiple times to corrupt storage
|
|
586
|
+
// ${contractName}(PROXY).${funcName}(...);
|
|
587
|
+
|
|
588
|
+
// 2. First call sets owner to legitimate address
|
|
589
|
+
// Second call can overwrite storage if unprotected
|
|
590
|
+
|
|
591
|
+
// 3. If storage is corrupted, attacker can:
|
|
592
|
+
// - Change owner to attacker address
|
|
593
|
+
// - Reset balances
|
|
594
|
+
// - Corrupt critical state variables
|
|
595
|
+
|
|
596
|
+
// Assert storage corruption
|
|
597
|
+
// assertEq(owner, attacker);
|
|
598
|
+
}
|
|
599
|
+
}`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Generate Foundry PoC for unauthorized upgrade
|
|
604
|
+
*/
|
|
605
|
+
generateUpgradePoC(contractName, funcName) {
|
|
606
|
+
return `// SPDX-License-Identifier: MIT
|
|
607
|
+
pragma solidity ^0.8.0;
|
|
608
|
+
|
|
609
|
+
import "forge-std/Test.sol";
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Proof of Concept: Unauthorized Upgrade Exploit
|
|
613
|
+
* Target: ${contractName}.${funcName}()
|
|
614
|
+
* Attack Vector: Upgrade to malicious implementation
|
|
615
|
+
*/
|
|
616
|
+
contract UpgradeExploit is Test {
|
|
617
|
+
address constant PROXY = address(0); // ${contractName} proxy address
|
|
618
|
+
MaliciousImplementation maliciousImpl;
|
|
619
|
+
|
|
620
|
+
function setUp() public {
|
|
621
|
+
maliciousImpl = new MaliciousImplementation();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function testExploit() public {
|
|
625
|
+
// 1. Deploy malicious implementation
|
|
626
|
+
// MaliciousImplementation impl = new MaliciousImplementation();
|
|
627
|
+
|
|
628
|
+
// 2. Call upgrade function without authorization
|
|
629
|
+
// ${contractName}(PROXY).${funcName}(address(impl));
|
|
630
|
+
|
|
631
|
+
// 3. Malicious implementation can:
|
|
632
|
+
// - Steal all funds via selfdestruct
|
|
633
|
+
// - Change owner
|
|
634
|
+
// - Break functionality
|
|
635
|
+
|
|
636
|
+
// Assert exploit succeeded
|
|
637
|
+
// assertEq(address(PROXY).balance, 0);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
contract MaliciousImplementation {
|
|
642
|
+
function initialize() external {
|
|
643
|
+
// Steal funds
|
|
644
|
+
selfdestruct(payable(msg.sender));
|
|
645
|
+
}
|
|
646
|
+
}`;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
module.exports = ProxyVulnerabilitiesDetector;
|
|
651
|
+
|