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,359 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Enhanced Reentrancy Detector
5
+ * Uses control flow and data flow analysis instead of simple pattern matching
6
+ * Tracks cross-function reentrancy and validates exploitability
7
+ */
8
+ class ReentrancyEnhancedDetector extends BaseDetector {
9
+ constructor() {
10
+ super(
11
+ 'Reentrancy Vulnerability (Enhanced)',
12
+ 'Advanced reentrancy detection using control/data flow analysis',
13
+ 'CRITICAL'
14
+ );
15
+ this.cfg = null;
16
+ this.dataFlow = null;
17
+ }
18
+
19
+ /**
20
+ * Override detect to use CFG and data flow analysis
21
+ */
22
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
23
+ this.cfg = cfg;
24
+ this.dataFlow = dataFlow;
25
+ this.sourceCode = sourceCode;
26
+ this.fileName = fileName;
27
+ this.sourceLines = sourceCode.split('\n');
28
+ this.findings = [];
29
+
30
+ if (!cfg || !dataFlow) {
31
+ // Fallback to basic detection if advanced analysis not available
32
+ return super.detect(ast, sourceCode, fileName);
33
+ }
34
+
35
+ // Analyze each function for reentrancy
36
+ for (const [funcKey, funcInfo] of cfg.functions) {
37
+ this.analyzeFunctionReentrancy(funcKey, funcInfo);
38
+ }
39
+
40
+ return this.findings;
41
+ }
42
+
43
+ /**
44
+ * Analyze a function for reentrancy vulnerabilities
45
+ */
46
+ analyzeFunctionReentrancy(funcKey, funcInfo) {
47
+ // Skip private/internal functions (not directly exploitable)
48
+ if (funcInfo.visibility === 'private' || funcInfo.visibility === 'internal') {
49
+ return;
50
+ }
51
+
52
+ // Check for classic reentrancy: external call before state write
53
+ const classicReentrancy = this.detectClassicReentrancy(funcInfo);
54
+ if (classicReentrancy) {
55
+ this.reportClassicReentrancy(funcInfo, classicReentrancy);
56
+ }
57
+
58
+ // Check for cross-function reentrancy
59
+ const crossFunction = this.detectCrossFunctionReentrancy(funcKey, funcInfo);
60
+ if (crossFunction) {
61
+ this.reportCrossFunctionReentrancy(funcInfo, crossFunction);
62
+ }
63
+
64
+ // Check for read-only reentrancy
65
+ const readOnly = this.detectReadOnlyReentrancy(funcKey, funcInfo);
66
+ if (readOnly) {
67
+ this.reportReadOnlyReentrancy(funcInfo, readOnly);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Detect classic reentrancy: external call before state modification
73
+ */
74
+ detectClassicReentrancy(funcInfo) {
75
+ const externalCalls = funcInfo.externalCalls;
76
+ const stateWrites = funcInfo.stateWrites;
77
+
78
+ // Check if any state write happens after an external call
79
+ for (const call of externalCalls) {
80
+ for (const write of stateWrites) {
81
+ if (call.loc && write.loc) {
82
+ // Compare positions: if call comes before write, it's vulnerable
83
+ if (this.comesBefore(call.loc, write.loc)) {
84
+ // Verify this is actually exploitable
85
+ if (this.isExploitable(funcInfo, call, write)) {
86
+ // Adjust confidence based on call type
87
+ // transfer/send have 2300 gas stipend but aren't fully safe post-Istanbul
88
+ const isLimitedGas = call.type === 'transfer' || call.type === 'send';
89
+ return {
90
+ call: call,
91
+ write: write,
92
+ severity: isLimitedGas ? 'HIGH' : 'CRITICAL',
93
+ confidence: isLimitedGas ? 'MEDIUM' : 'HIGH'
94
+ };
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Detect cross-function reentrancy
106
+ * Function A makes external call, allowing reentrant call to Function B
107
+ * which modifies shared state
108
+ */
109
+ detectCrossFunctionReentrancy(funcKey, funcInfo) {
110
+ if (funcInfo.externalCalls.length === 0) {
111
+ return null;
112
+ }
113
+
114
+ // Find other public/external functions in same contract that modify state
115
+ const contractFunctions = Array.from(this.cfg.functions.values())
116
+ .filter(f => f.contract === funcInfo.contract)
117
+ .filter(f => f.visibility === 'public' || f.visibility === 'external')
118
+ .filter(f => f.name !== funcInfo.name);
119
+
120
+ for (const otherFunc of contractFunctions) {
121
+ // Check if other function modifies state that this function depends on
122
+ const sharedState = this.findSharedStateVariables(funcInfo, otherFunc);
123
+
124
+ if (sharedState.length > 0) {
125
+ // Check if this function lacks reentrancy protection
126
+ if (!this.hasReentrancyGuard(funcInfo) && !this.hasReentrancyGuard(otherFunc)) {
127
+ return {
128
+ vulnerableFunc: funcInfo,
129
+ reentrantFunc: otherFunc,
130
+ sharedState: sharedState,
131
+ severity: 'CRITICAL',
132
+ confidence: 'MEDIUM'
133
+ };
134
+ }
135
+ }
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * Detect read-only reentrancy
143
+ * External call in a view/getter allowing state inconsistency
144
+ *
145
+ * HARDENED: Only report if:
146
+ * 1. Function is used by external protocols (has dependent contracts)
147
+ * 2. State read is for value calculation (balances, shares, prices)
148
+ * 3. High confidence of actual exploitation path
149
+ *
150
+ * We skip low-confidence read-only reentrancy as it rarely leads to direct fund theft
151
+ * and creates noise. The Curve LP oracle attack was specific to cross-contract composition.
152
+ */
153
+ detectReadOnlyReentrancy(funcKey, funcInfo) {
154
+ // Skip low-value read-only reentrancy detection to reduce noise
155
+ // Read-only reentrancy requires:
156
+ // 1. External contract depending on this view function
157
+ // 2. View function reading state that can be manipulated mid-call
158
+ // 3. External contract making financial decisions based on stale view
159
+ //
160
+ // Without cross-contract analysis, we cannot reliably detect this.
161
+ // Keeping this disabled to optimize for precision over recall.
162
+ //
163
+ // For protocols that need this, recommend manual audit of view functions
164
+ // called by external protocols (oracles, composability).
165
+
166
+ // Only report CRITICAL cases: view/pure function making external calls
167
+ // This is a code smell that violates the view/pure guarantee
168
+ if (funcInfo.stateMutability === 'view' || funcInfo.stateMutability === 'pure') {
169
+ if (funcInfo.externalCalls.length > 0) {
170
+ // Check if any external call could trigger reentrancy to state-modifying function
171
+ const hasCallWithValue = funcInfo.externalCalls.some(c =>
172
+ c.type === 'call' || c.type === 'delegatecall'
173
+ );
174
+
175
+ if (hasCallWithValue) {
176
+ // Only report if function name suggests it's used for pricing/valuation
177
+ const funcNameLower = funcInfo.name.toLowerCase();
178
+ const isPricingFunction = /price|value|balance|amount|rate|convert|preview|quote|get.*assets|get.*shares/i.test(funcNameLower);
179
+
180
+ if (isPricingFunction) {
181
+ return {
182
+ func: funcInfo,
183
+ calls: funcInfo.externalCalls,
184
+ severity: 'HIGH',
185
+ confidence: 'MEDIUM'
186
+ };
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ // Do NOT report low-confidence read-only reentrancy (state read after external call)
193
+ // This pattern is too common and rarely exploitable without cross-contract context
194
+ return null;
195
+ }
196
+
197
+ /**
198
+ * Check if reentrancy is actually exploitable
199
+ * Returns object with exploitability details for severity adjustment
200
+ */
201
+ isExploitable(funcInfo, call, write) {
202
+ // Check 1: Has reentrancy guard?
203
+ if (this.hasReentrancyGuard(funcInfo)) {
204
+ return false;
205
+ }
206
+
207
+ // Check 2: Is function publicly accessible?
208
+ if (funcInfo.visibility === 'private' || funcInfo.visibility === 'internal') {
209
+ return false;
210
+ }
211
+
212
+ // Does external call allow reentrancy?
213
+ // Note: Post-Istanbul (EIP-1884), transfer/send with 2300 gas stipend
214
+ // are less reliable due to increased SLOAD costs. While they provide
215
+ // some protection, they should not be considered fully safe.
216
+ // We still report these but with adjusted confidence.
217
+ if (call.type === 'transfer' || call.type === 'send') {
218
+ // Return true but caller should adjust confidence to MEDIUM
219
+ // These have gas limits but are not guaranteed safe post-Istanbul
220
+ return true;
221
+ }
222
+
223
+ return true;
224
+ }
225
+
226
+ /**
227
+ * Find state variables modified by both functions
228
+ */
229
+ findSharedStateVariables(func1, func2) {
230
+ const shared = [];
231
+
232
+ const writes1 = new Set(func1.stateWrites.map(w => w.variable));
233
+ const writes2 = new Set(func2.stateWrites.map(w => w.variable));
234
+
235
+ for (const varName of writes1) {
236
+ if (writes2.has(varName)) {
237
+ shared.push(varName);
238
+ }
239
+ }
240
+
241
+ return shared;
242
+ }
243
+
244
+ /**
245
+ * Check if function has reentrancy protection
246
+ */
247
+ hasReentrancyGuard(funcInfo) {
248
+ // Check modifiers
249
+ for (const modName of funcInfo.modifiers) {
250
+ const modKey = `${funcInfo.contract}.${modName}`;
251
+ const modInfo = this.cfg.modifiers.get(modKey);
252
+
253
+ if (modInfo) {
254
+ // Check if modifier implements reentrancy guard pattern
255
+ const hasLockCheck = modInfo.requireStatements.some(stmt =>
256
+ stmt.includes('_status') ||
257
+ stmt.includes('locked') ||
258
+ stmt.includes('_notEntered')
259
+ );
260
+
261
+ if (hasLockCheck) {
262
+ return true;
263
+ }
264
+
265
+ // Check modifier name
266
+ const modNameLower = modName.toLowerCase();
267
+ if (modNameLower.includes('nonreentrant') ||
268
+ modNameLower.includes('lock') ||
269
+ modNameLower.includes('guard')) {
270
+ return true;
271
+ }
272
+ }
273
+ }
274
+
275
+ return false;
276
+ }
277
+
278
+ /**
279
+ * Check if location A comes before location B in source code
280
+ */
281
+ comesBefore(locA, locB) {
282
+ if (!locA || !locB) return false;
283
+
284
+ if (locA.start.line < locB.start.line) {
285
+ return true;
286
+ } else if (locA.start.line === locB.start.line) {
287
+ return locA.start.column < locB.start.column;
288
+ }
289
+
290
+ return false;
291
+ }
292
+
293
+ /**
294
+ * Report classic reentrancy finding
295
+ */
296
+ reportClassicReentrancy(funcInfo, vuln) {
297
+ this.addFinding({
298
+ title: 'Classic Reentrancy Vulnerability',
299
+ description: `Function '${funcInfo.name}' performs external ${vuln.call.type} before updating state variable '${vuln.write.variable}'. This allows attackers to reenter the function and exploit the stale state. EXPLOITABLE: No reentrancy guard detected.`,
300
+ location: `Contract: ${funcInfo.contract}, Function: ${funcInfo.name}`,
301
+ line: vuln.call.loc ? vuln.call.loc.start.line : 0,
302
+ column: vuln.call.loc ? vuln.call.loc.start.column : 0,
303
+ code: this.getCodeSnippet(vuln.call.loc),
304
+ severity: vuln.severity,
305
+ confidence: vuln.confidence,
306
+ exploitable: true,
307
+ recommendation: 'CRITICAL FIX REQUIRED: Move state updates before external calls (Checks-Effects-Interactions pattern) OR add nonReentrant modifier from OpenZeppelin ReentrancyGuard.',
308
+ references: [
309
+ 'https://consensys.github.io/smart-contract-best-practices/attacks/reentrancy/',
310
+ 'https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard'
311
+ ]
312
+ });
313
+ }
314
+
315
+ /**
316
+ * Report cross-function reentrancy finding
317
+ */
318
+ reportCrossFunctionReentrancy(funcInfo, vuln) {
319
+ this.addFinding({
320
+ title: 'Cross-Function Reentrancy Vulnerability',
321
+ description: `Function '${funcInfo.name}' makes external calls without reentrancy protection, allowing reentrant calls to '${vuln.reentrantFunc.name}' which modifies shared state: ${vuln.sharedState.join(', ')}. This is exploitable even without classic reentrancy pattern.`,
322
+ location: `Contract: ${funcInfo.contract}, Functions: ${funcInfo.name} <-> ${vuln.reentrantFunc.name}`,
323
+ line: funcInfo.node.loc ? funcInfo.node.loc.start.line : 0,
324
+ column: funcInfo.node.loc ? funcInfo.node.loc.start.column : 0,
325
+ code: this.getCodeSnippet(funcInfo.node.loc),
326
+ severity: vuln.severity,
327
+ confidence: vuln.confidence,
328
+ exploitable: true,
329
+ recommendation: 'Add nonReentrant modifier to ALL public/external functions that modify state or make external calls. Cross-function reentrancy requires contract-wide protection.',
330
+ references: [
331
+ 'https://github.com/pcaversaccio/reentrancy-attacks#cross-function-reentrancy',
332
+ 'https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard'
333
+ ]
334
+ });
335
+ }
336
+
337
+ /**
338
+ * Report read-only reentrancy finding
339
+ */
340
+ reportReadOnlyReentrancy(funcInfo, vuln) {
341
+ this.addFinding({
342
+ title: 'Read-Only Reentrancy Risk',
343
+ description: `Function '${funcInfo.name}' makes external calls and then reads state. While this function doesn't modify state, it can return inconsistent data if reentered, potentially affecting other contracts that depend on it.`,
344
+ location: `Contract: ${funcInfo.contract}, Function: ${funcInfo.name}`,
345
+ line: vuln.call?.loc ? vuln.call.loc.start.line : 0,
346
+ column: vuln.call?.loc ? vuln.call.loc.start.column : 0,
347
+ code: this.getCodeSnippet(vuln.call?.loc || funcInfo.node.loc),
348
+ severity: vuln.severity,
349
+ confidence: vuln.confidence,
350
+ exploitable: false,
351
+ recommendation: 'If this function is used by other contracts for critical decisions, add reentrancy protection. Consider using snapshot-based reads or reentrancy guards.',
352
+ references: [
353
+ 'https://chainsecurity.com/curve-lp-oracle-manipulation-post-mortem/'
354
+ ]
355
+ });
356
+ }
357
+ }
358
+
359
+ module.exports = ReentrancyEnhancedDetector;
@@ -0,0 +1,259 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Unprotected Selfdestruct Detector (Hardened)
5
+ *
6
+ * Only reports CRITICAL findings with concrete fund-theft impact:
7
+ * - Unprotected selfdestruct (anyone can destroy and steal funds)
8
+ * - Weak access control on selfdestruct (bypassable protection)
9
+ *
10
+ * Does NOT report:
11
+ * - Properly protected selfdestruct (admin-only with valid checks)
12
+ * - Informational "selfdestruct present" notices
13
+ */
14
+ class UnprotectedSelfdestructDetector extends BaseDetector {
15
+ constructor() {
16
+ super(
17
+ 'Unprotected Selfdestruct',
18
+ 'Detects exploitable selfdestruct calls that enable fund theft',
19
+ 'CRITICAL'
20
+ );
21
+ this.reportedLocations = new Set(); // Dedupe by location
22
+ }
23
+
24
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
25
+ this.reportedLocations = new Set();
26
+ return super.detect(ast, sourceCode, fileName, cfg, dataFlow);
27
+ }
28
+
29
+ visitFunctionDefinition(node) {
30
+ // Store current function context for nested checks
31
+ // NOTE: Do NOT call this.traverse() here - base class handles traversal
32
+ this.currentFunction = node;
33
+ }
34
+
35
+ visitFunctionCall(node) {
36
+ const code = this.getCodeSnippet(node.loc);
37
+ const line = node.loc ? node.loc.start.line : 0;
38
+
39
+ // Check for selfdestruct or suicide (deprecated)
40
+ if (code.includes('selfdestruct(') || code.includes('suicide(')) {
41
+ // Dedupe: only report once per line
42
+ const locationKey = `${this.fileName}:${line}`;
43
+ if (this.reportedLocations.has(locationKey)) {
44
+ return;
45
+ }
46
+ this.reportedLocations.add(locationKey);
47
+
48
+ this.checkSelfdestructCall(node, code, line);
49
+ }
50
+ }
51
+
52
+ checkSelfdestructCall(node, code, line) {
53
+ const functionContext = this.currentFunction;
54
+ const functionName = functionContext?.name || 'fallback/receive';
55
+ const visibility = functionContext?.visibility || 'public';
56
+
57
+ // Skip private/internal functions (not directly exploitable)
58
+ if (visibility === 'private' || visibility === 'internal') {
59
+ return;
60
+ }
61
+
62
+ // Check access control quality
63
+ const accessAnalysis = this.analyzeAccessControl(functionContext, code);
64
+
65
+ // Determine if user controls recipient (increases severity)
66
+ const userControlledRecipient = this.isUserControlledRecipient(code);
67
+
68
+ if (accessAnalysis.level === 'none') {
69
+ // CRITICAL: Completely unprotected selfdestruct
70
+ this.addFinding({
71
+ title: 'Unprotected Selfdestruct',
72
+ description: `Function '${functionName}' contains selfdestruct without ANY access control. ` +
73
+ `Any user can destroy the contract and ${userControlledRecipient ? 'redirect all funds to their address' : 'steal all funds'}.\n\n` +
74
+ `Attack: Simply call ${functionName}() to destroy contract and extract ${userControlledRecipient ? 'funds to attacker address' : 'all ETH'}.`,
75
+ location: `Function: ${functionName}`,
76
+ line: line,
77
+ column: node.loc ? node.loc.start.column : 0,
78
+ code: code,
79
+ severity: 'CRITICAL',
80
+ confidence: 'HIGH',
81
+ exploitable: true,
82
+ exploitabilityScore: 100,
83
+ attackVector: 'selfdestruct',
84
+ recommendation: 'Remove selfdestruct entirely, or add strict multi-sig + timelock protection. Note: selfdestruct is deprecated (EIP-6049) and will change behavior in future upgrades.',
85
+ references: [
86
+ 'https://swcregistry.io/docs/SWC-106',
87
+ 'https://eips.ethereum.org/EIPS/eip-6049'
88
+ ],
89
+ foundryPoC: this.generateSelfdestructPoC(functionName, userControlledRecipient)
90
+ });
91
+ } else if (accessAnalysis.level === 'broken') {
92
+ // CRITICAL: Access control exists but is broken/bypassable
93
+ this.addFinding({
94
+ title: 'Broken Access Control on Selfdestruct',
95
+ description: `Function '${functionName}' has selfdestruct with BROKEN access control: ${accessAnalysis.reason}. ` +
96
+ `Attacker can bypass the check and destroy the contract.\n\n` +
97
+ `Vulnerability: ${accessAnalysis.reason}`,
98
+ location: `Function: ${functionName}`,
99
+ line: line,
100
+ column: node.loc ? node.loc.start.column : 0,
101
+ code: code,
102
+ severity: 'CRITICAL',
103
+ confidence: 'HIGH',
104
+ exploitable: true,
105
+ exploitabilityScore: 95,
106
+ attackVector: 'selfdestruct',
107
+ recommendation: `Fix the access control: ${accessAnalysis.fix}`,
108
+ references: [
109
+ 'https://swcregistry.io/docs/SWC-106'
110
+ ]
111
+ });
112
+ } else if (accessAnalysis.level === 'weak') {
113
+ // HIGH: Weak access control (tx.origin, balance-based, timestamp)
114
+ this.addFinding({
115
+ title: 'Weak Access Control on Selfdestruct',
116
+ description: `Function '${functionName}' has selfdestruct with WEAK access control: ${accessAnalysis.reason}. ` +
117
+ `This may be exploitable under certain conditions.`,
118
+ location: `Function: ${functionName}`,
119
+ line: line,
120
+ column: node.loc ? node.loc.start.column : 0,
121
+ code: code,
122
+ severity: 'HIGH',
123
+ confidence: 'MEDIUM',
124
+ exploitable: true,
125
+ exploitabilityScore: 75,
126
+ attackVector: 'selfdestruct',
127
+ recommendation: `Strengthen access control: ${accessAnalysis.fix}`,
128
+ references: [
129
+ 'https://swcregistry.io/docs/SWC-106'
130
+ ]
131
+ });
132
+ }
133
+ // Note: Strong access control = no finding (not a vulnerability)
134
+ }
135
+
136
+ analyzeAccessControl(functionNode, code) {
137
+ if (!functionNode) {
138
+ return { level: 'none', reason: 'No function context (fallback/receive)' };
139
+ }
140
+
141
+ // Check for modifiers
142
+ const modifiers = functionNode.modifiers || [];
143
+ if (modifiers.length === 0) {
144
+ // Check for inline require statements
145
+ if (this.hasInlineAccessControl(code)) {
146
+ return this.analyzeInlineAccessControl(code);
147
+ }
148
+ return { level: 'none', reason: 'No access control modifiers or require checks' };
149
+ }
150
+
151
+ // Analyze modifier quality
152
+ for (const modifier of modifiers) {
153
+ const modName = (modifier.name || '').toLowerCase();
154
+
155
+ // Check for known strong patterns
156
+ if (/^only(owner|admin|governance|role)$/i.test(modName)) {
157
+ // Need to verify the modifier actually works (check CFG if available)
158
+ if (this.cfg) {
159
+ const modKey = `${this.currentContract}.${modifier.name}`;
160
+ const modInfo = this.cfg.modifiers?.get(modKey);
161
+ if (modInfo && modInfo.requireStatements.length === 0) {
162
+ return { level: 'broken', reason: 'Modifier has empty body - no actual check', fix: 'Implement proper ownership check in modifier' };
163
+ }
164
+ }
165
+ return { level: 'strong', reason: 'Protected by ownership modifier' };
166
+ }
167
+ }
168
+
169
+ // Check code for weak patterns
170
+ return this.analyzeInlineAccessControl(code);
171
+ }
172
+
173
+ hasInlineAccessControl(code) {
174
+ return /require\s*\(|if\s*\(.*revert/i.test(code);
175
+ }
176
+
177
+ analyzeInlineAccessControl(code) {
178
+ const codeLower = code.toLowerCase();
179
+
180
+ // Check for tx.origin (phishing vulnerable)
181
+ if (/require\s*\(\s*tx\.origin\s*==|tx\.origin\s*==.*require/i.test(code)) {
182
+ return { level: 'weak', reason: 'Uses tx.origin (vulnerable to phishing attacks)', fix: 'Use msg.sender instead of tx.origin' };
183
+ }
184
+
185
+ // Check for timestamp-based (manipulable)
186
+ if (/require\s*\(.*block\.timestamp|block\.timestamp.*require/i.test(code)) {
187
+ return { level: 'weak', reason: 'Uses block.timestamp (manipulable by miners, will eventually pass)', fix: 'Use proper ownership check, not time-based' };
188
+ }
189
+
190
+ // Check for balance-based (flash loan vulnerable)
191
+ if (/require\s*\(.*\.balance\s*>|\.balance\s*>=.*require/i.test(code)) {
192
+ return { level: 'weak', reason: 'Uses balance-based access control (flash loan vulnerable)', fix: 'Use ownership check, not balance-based' };
193
+ }
194
+
195
+ // Check for proper msg.sender check
196
+ if (/require\s*\(\s*msg\.sender\s*==\s*(owner|admin|_owner)/i.test(code)) {
197
+ return { level: 'strong', reason: 'Protected by msg.sender ownership check' };
198
+ }
199
+
200
+ // Check for always-true conditions
201
+ if (/require\s*\(\s*true\s*\)|require\s*\(\s*1\s*==\s*1\s*\)/i.test(code)) {
202
+ return { level: 'broken', reason: 'Always-true require condition', fix: 'Implement actual access control check' };
203
+ }
204
+
205
+ // Has require but unclear what it checks
206
+ if (/require\s*\(/i.test(code)) {
207
+ return { level: 'unknown', reason: 'Has require but unclear protection' };
208
+ }
209
+
210
+ return { level: 'none', reason: 'No access control detected' };
211
+ }
212
+
213
+ isUserControlledRecipient(code) {
214
+ // Check if recipient is user-controllable
215
+ const userControlledPatterns = [
216
+ /selfdestruct\s*\(\s*payable\s*\(\s*msg\.sender\s*\)\s*\)/,
217
+ /selfdestruct\s*\(\s*msg\.sender\s*\)/,
218
+ /selfdestruct\s*\(\s*payable\s*\(\s*[_a-zA-Z]\w*\s*\)\s*\)/, // Parameter
219
+ /selfdestruct\s*\(\s*[_a-zA-Z]\w*\s*\)/ // Parameter without payable
220
+ ];
221
+ return userControlledPatterns.some(p => p.test(code));
222
+ }
223
+
224
+ generateSelfdestructPoC(functionName, userControlled) {
225
+ return `// SPDX-License-Identifier: MIT
226
+ pragma solidity ^0.8.0;
227
+
228
+ import "forge-std/Test.sol";
229
+
230
+ /**
231
+ * PoC: Unprotected Selfdestruct Exploit
232
+ * Demonstrates complete fund theft via unprotected selfdestruct
233
+ */
234
+ contract SelfdestructExploit is Test {
235
+ address victim;
236
+ address attacker = address(0xBAD);
237
+
238
+ function setUp() public {
239
+ // Deploy victim contract and fund it
240
+ // victim = address(new VictimContract());
241
+ // vm.deal(victim, 100 ether);
242
+ }
243
+
244
+ function testExploit() public {
245
+ uint256 victimBalanceBefore = victim.balance;
246
+ uint256 attackerBalanceBefore = attacker.balance;
247
+
248
+ vm.prank(attacker);
249
+ // VictimContract(victim).${functionName}(${userControlled ? 'payable(attacker)' : ''});
250
+
251
+ // Assert: Contract destroyed, funds stolen
252
+ // assertEq(victim.code.length, 0, "Contract should be destroyed");
253
+ // assertEq(attacker.balance, attackerBalanceBefore + victimBalanceBefore, "Attacker should have stolen funds");
254
+ }
255
+ }`;
256
+ }
257
+ }
258
+
259
+ module.exports = UnprotectedSelfdestructDetector;