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,701 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gas Griefing and DoS Detector (Enhanced)
|
|
5
|
+
* Detects patterns vulnerable to denial of service via gas manipulation
|
|
6
|
+
* with improved loop bound analysis to reduce false positives.
|
|
7
|
+
*/
|
|
8
|
+
class GasGriefingDetector extends BaseDetector {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(
|
|
11
|
+
'Gas Griefing / DoS',
|
|
12
|
+
'Detects patterns vulnerable to gas-based denial of service',
|
|
13
|
+
'HIGH'
|
|
14
|
+
);
|
|
15
|
+
this.currentContract = null;
|
|
16
|
+
this.currentFunction = null;
|
|
17
|
+
this.currentFunctionNode = null;
|
|
18
|
+
this.isInLoop = false;
|
|
19
|
+
this.loopDepth = 0;
|
|
20
|
+
this.currentLoopIsUnbounded = false;
|
|
21
|
+
|
|
22
|
+
// Safe iteration limit - loops with bounds under this are considered safe
|
|
23
|
+
this.SAFE_ITERATION_LIMIT = 100;
|
|
24
|
+
// Warning threshold - bounded but could still be expensive
|
|
25
|
+
this.WARNING_ITERATION_LIMIT = 1000;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async detect(ast, sourceCode, fileName, cfg, dataFlow) {
|
|
29
|
+
this.findings = [];
|
|
30
|
+
this.ast = ast;
|
|
31
|
+
this.sourceCode = sourceCode;
|
|
32
|
+
this.fileName = fileName;
|
|
33
|
+
this.sourceLines = sourceCode.split('\n');
|
|
34
|
+
|
|
35
|
+
this.traverse(ast);
|
|
36
|
+
|
|
37
|
+
return this.findings;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
visitContractDefinition(node) {
|
|
41
|
+
this.currentContract = node.name;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
visitFunctionDefinition(node) {
|
|
45
|
+
this.currentFunction = node.name || 'constructor';
|
|
46
|
+
this.currentFunctionNode = node;
|
|
47
|
+
this.isInLoop = false;
|
|
48
|
+
this.loopDepth = 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
visitForStatement(node) {
|
|
52
|
+
this.loopDepth++;
|
|
53
|
+
this.isInLoop = true;
|
|
54
|
+
|
|
55
|
+
// Analyze the loop with context awareness
|
|
56
|
+
this.analyzeForLoop(node);
|
|
57
|
+
|
|
58
|
+
// After processing children
|
|
59
|
+
this.loopDepth--;
|
|
60
|
+
if (this.loopDepth === 0) {
|
|
61
|
+
this.isInLoop = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
visitWhileStatement(node) {
|
|
66
|
+
this.loopDepth++;
|
|
67
|
+
this.isInLoop = true;
|
|
68
|
+
|
|
69
|
+
// Analyze while loop - check if it has proper bounds
|
|
70
|
+
this.analyzeWhileLoop(node);
|
|
71
|
+
|
|
72
|
+
this.loopDepth--;
|
|
73
|
+
if (this.loopDepth === 0) {
|
|
74
|
+
this.isInLoop = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
visitFunctionCall(node) {
|
|
79
|
+
if (!node.expression) return;
|
|
80
|
+
|
|
81
|
+
const code = this.getCodeSnippet(node.loc);
|
|
82
|
+
|
|
83
|
+
// Check for external calls in loops (only if unbounded)
|
|
84
|
+
if (this.isInLoop && this.currentLoopIsUnbounded) {
|
|
85
|
+
if (code.includes('.call') || code.includes('.transfer') || code.includes('.send')) {
|
|
86
|
+
this.reportExternalCallInLoop(node);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check for push to dynamic array - only if it's user-controlled
|
|
91
|
+
if (node.expression.type === 'MemberAccess' && node.expression.memberName === 'push') {
|
|
92
|
+
this.checkArrayPush(node);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Analyze for loop with comprehensive bound checking
|
|
98
|
+
*/
|
|
99
|
+
analyzeForLoop(node) {
|
|
100
|
+
const code = this.getCodeSnippet(node.loc);
|
|
101
|
+
const funcCode = this.currentFunctionNode ? this.getCodeSnippet(this.currentFunctionNode.loc) : code;
|
|
102
|
+
|
|
103
|
+
// Extract loop bounds analysis
|
|
104
|
+
const boundAnalysis = this.analyzeLoopBounds(node, code, funcCode);
|
|
105
|
+
|
|
106
|
+
// Track if this loop is unbounded for nested analysis
|
|
107
|
+
this.currentLoopIsUnbounded = !boundAnalysis.hasBound;
|
|
108
|
+
|
|
109
|
+
if (boundAnalysis.hasBound) {
|
|
110
|
+
// Loop has explicit bounds
|
|
111
|
+
if (boundAnalysis.boundValue <= this.SAFE_ITERATION_LIMIT) {
|
|
112
|
+
// Safe - small fixed bound, no report needed
|
|
113
|
+
return;
|
|
114
|
+
} else if (boundAnalysis.boundValue <= this.WARNING_ITERATION_LIMIT) {
|
|
115
|
+
// Warning - moderate bound
|
|
116
|
+
if (this.hasGasHeavyOperations(code)) {
|
|
117
|
+
this.reportBoundedButExpensive(node, boundAnalysis);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Large bound - continue to check if it's problematic
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check for dynamic array iteration
|
|
125
|
+
const dynamicArrayAnalysis = this.analyzeDynamicArrayIteration(code, funcCode);
|
|
126
|
+
|
|
127
|
+
if (dynamicArrayAnalysis.iteratesOverDynamicArray) {
|
|
128
|
+
// Check if there's pagination or safe guards
|
|
129
|
+
if (dynamicArrayAnalysis.hasSafeGuards) {
|
|
130
|
+
// Has pagination, batching, or limits - safe
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if it's truly unbounded and user-controllable
|
|
135
|
+
if (dynamicArrayAnalysis.isUserControllable) {
|
|
136
|
+
this.reportUnboundedIteration(node, dynamicArrayAnalysis);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check for gas-heavy operations in unbounded loops
|
|
141
|
+
if (!boundAnalysis.hasBound && this.hasGasHeavyOperations(code)) {
|
|
142
|
+
this.reportGasHeavyLoop(node);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Analyze while loop for proper termination bounds
|
|
148
|
+
*/
|
|
149
|
+
analyzeWhileLoop(node) {
|
|
150
|
+
const code = this.getCodeSnippet(node.loc);
|
|
151
|
+
const funcCode = this.currentFunctionNode ? this.getCodeSnippet(this.currentFunctionNode.loc) : code;
|
|
152
|
+
|
|
153
|
+
// Check for counter-based while loops (actually bounded)
|
|
154
|
+
const hasCounterBound = this.hasCounterBasedBound(code);
|
|
155
|
+
|
|
156
|
+
if (hasCounterBound) {
|
|
157
|
+
// While loop with explicit counter check - analyze the bound
|
|
158
|
+
const boundAnalysis = this.analyzeWhileBounds(code, funcCode);
|
|
159
|
+
|
|
160
|
+
if (boundAnalysis.hasBound && boundAnalysis.boundValue <= this.WARNING_ITERATION_LIMIT) {
|
|
161
|
+
// Bounded while loop - safe
|
|
162
|
+
this.currentLoopIsUnbounded = false;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check for state-based termination (e.g., queue processing)
|
|
168
|
+
const hasStateTermination = this.hasStateBasedTermination(code);
|
|
169
|
+
|
|
170
|
+
if (hasStateTermination) {
|
|
171
|
+
// While loop processes state that will eventually terminate
|
|
172
|
+
// Only flag if it has gas-heavy operations
|
|
173
|
+
if (this.hasGasHeavyOperations(code)) {
|
|
174
|
+
this.reportPotentiallyUnboundedWhile(node);
|
|
175
|
+
}
|
|
176
|
+
this.currentLoopIsUnbounded = true;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check for infinite loop patterns (truly dangerous)
|
|
181
|
+
const hasInfinitePattern = this.hasInfiniteLoopPattern(code);
|
|
182
|
+
|
|
183
|
+
if (hasInfinitePattern) {
|
|
184
|
+
this.reportInfiniteLoop(node);
|
|
185
|
+
this.currentLoopIsUnbounded = true;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Default: flag if no clear termination condition
|
|
190
|
+
this.currentLoopIsUnbounded = true;
|
|
191
|
+
if (this.hasGasHeavyOperations(code)) {
|
|
192
|
+
this.reportUnboundedWhileLoop(node);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Analyze loop bounds from for-loop structure
|
|
198
|
+
*/
|
|
199
|
+
analyzeLoopBounds(node, code, funcCode) {
|
|
200
|
+
const result = {
|
|
201
|
+
hasBound: false,
|
|
202
|
+
boundValue: Infinity,
|
|
203
|
+
boundSource: 'unknown'
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Pattern 1: Explicit numeric bound (for i = 0; i < 10; i++)
|
|
207
|
+
const numericBound = code.match(/[<>=]\s*(\d+)/);
|
|
208
|
+
if (numericBound) {
|
|
209
|
+
result.hasBound = true;
|
|
210
|
+
result.boundValue = parseInt(numericBound[1]);
|
|
211
|
+
result.boundSource = 'literal';
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Pattern 2: Constant bound (for i = 0; i < MAX_ITERATIONS; i++)
|
|
216
|
+
const constantPattern = /[<>=]\s*([A-Z_][A-Z0-9_]*)/;
|
|
217
|
+
const constantMatch = code.match(constantPattern);
|
|
218
|
+
if (constantMatch) {
|
|
219
|
+
const constantName = constantMatch[1];
|
|
220
|
+
// Try to find constant definition
|
|
221
|
+
const constantDef = funcCode.match(new RegExp(`${constantName}\\s*=\\s*(\\d+)`));
|
|
222
|
+
if (constantDef) {
|
|
223
|
+
result.hasBound = true;
|
|
224
|
+
result.boundValue = parseInt(constantDef[1]);
|
|
225
|
+
result.boundSource = 'constant';
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
// Known safe constants
|
|
229
|
+
if (/^MAX_|^LIMIT_|^BATCH_SIZE/i.test(constantName)) {
|
|
230
|
+
result.hasBound = true;
|
|
231
|
+
result.boundValue = this.WARNING_ITERATION_LIMIT; // Assume reasonable
|
|
232
|
+
result.boundSource = 'constant';
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Pattern 3: Min/Math.min bound
|
|
238
|
+
if (/Math\.min|min\s*\(/i.test(code)) {
|
|
239
|
+
result.hasBound = true;
|
|
240
|
+
result.boundValue = this.WARNING_ITERATION_LIMIT;
|
|
241
|
+
result.boundSource = 'min-function';
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Pattern 4: Parameter with require check
|
|
246
|
+
const paramBound = code.match(/[<>=]\s*(\w+)/);
|
|
247
|
+
if (paramBound) {
|
|
248
|
+
const boundVar = paramBound[1];
|
|
249
|
+
// Check if there's a require limiting this parameter
|
|
250
|
+
const requirePattern = new RegExp(`require\\s*\\([^;]*${boundVar}[^;]*[<>=]\\s*(\\d+)`);
|
|
251
|
+
const requireMatch = funcCode.match(requirePattern);
|
|
252
|
+
if (requireMatch) {
|
|
253
|
+
result.hasBound = true;
|
|
254
|
+
result.boundValue = parseInt(requireMatch[1]);
|
|
255
|
+
result.boundSource = 'require-bounded';
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Analyze while loop bounds
|
|
265
|
+
*/
|
|
266
|
+
analyzeWhileBounds(code, funcCode) {
|
|
267
|
+
const result = {
|
|
268
|
+
hasBound: false,
|
|
269
|
+
boundValue: Infinity
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Check for counter increment patterns
|
|
273
|
+
const counterPatterns = [
|
|
274
|
+
/(\w+)\s*\+\+/, // i++
|
|
275
|
+
/(\w+)\s*\+=\s*1/, // i += 1
|
|
276
|
+
/(\w+)\s*=\s*\1\s*\+\s*1/ // i = i + 1
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
let counterVar = null;
|
|
280
|
+
for (const pattern of counterPatterns) {
|
|
281
|
+
const match = code.match(pattern);
|
|
282
|
+
if (match) {
|
|
283
|
+
counterVar = match[1];
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (counterVar) {
|
|
289
|
+
// Find the bound for this counter
|
|
290
|
+
const boundPattern = new RegExp(`${counterVar}\\s*[<>=]+\\s*(\\d+|[A-Z_]+)`);
|
|
291
|
+
const boundMatch = code.match(boundPattern);
|
|
292
|
+
|
|
293
|
+
if (boundMatch) {
|
|
294
|
+
const boundValue = parseInt(boundMatch[1]);
|
|
295
|
+
if (!isNaN(boundValue)) {
|
|
296
|
+
result.hasBound = true;
|
|
297
|
+
result.boundValue = boundValue;
|
|
298
|
+
} else {
|
|
299
|
+
// It's a constant, assume bounded
|
|
300
|
+
result.hasBound = true;
|
|
301
|
+
result.boundValue = this.WARNING_ITERATION_LIMIT;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Check for counter-based bounds in while loops
|
|
311
|
+
*/
|
|
312
|
+
hasCounterBasedBound(code) {
|
|
313
|
+
// Look for patterns like: while (i < limit) with i++ inside
|
|
314
|
+
const hasComparison = /while\s*\([^)]*[<>=]/i.test(code);
|
|
315
|
+
const hasIncrement = /\+\+|\+=\s*1|=\s*\w+\s*\+\s*1/i.test(code);
|
|
316
|
+
|
|
317
|
+
return hasComparison && hasIncrement;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Check for state-based termination
|
|
322
|
+
*/
|
|
323
|
+
hasStateBasedTermination(code) {
|
|
324
|
+
// Queue/stack processing patterns
|
|
325
|
+
const statePatterns = [
|
|
326
|
+
/\.length\s*>\s*0/, // while (queue.length > 0)
|
|
327
|
+
/\.pop\s*\(/, // with pop operation
|
|
328
|
+
/\.shift\s*\(/, // with shift operation
|
|
329
|
+
/isEmpty/i, // isEmpty check
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
return statePatterns.some(p => p.test(code));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Check for infinite loop patterns
|
|
337
|
+
*/
|
|
338
|
+
hasInfiniteLoopPattern(code) {
|
|
339
|
+
// while(true), while(1), for(;;)
|
|
340
|
+
return /while\s*\(\s*(true|1)\s*\)|for\s*\(\s*;\s*;\s*\)/.test(code);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Analyze if loop iterates over dynamic, user-controllable array
|
|
345
|
+
*/
|
|
346
|
+
analyzeDynamicArrayIteration(code, funcCode) {
|
|
347
|
+
const result = {
|
|
348
|
+
iteratesOverDynamicArray: false,
|
|
349
|
+
arrayName: null,
|
|
350
|
+
isUserControllable: false,
|
|
351
|
+
hasSafeGuards: false
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Check for .length iteration
|
|
355
|
+
const lengthMatch = code.match(/(\w+)\.length/);
|
|
356
|
+
if (!lengthMatch) return result;
|
|
357
|
+
|
|
358
|
+
result.iteratesOverDynamicArray = true;
|
|
359
|
+
result.arrayName = lengthMatch[1];
|
|
360
|
+
|
|
361
|
+
// Check if array is user-controllable
|
|
362
|
+
const userControllablePatterns = [
|
|
363
|
+
/public\s+\w+\[\]/, // public array
|
|
364
|
+
/push.*msg\.sender/, // users can add themselves
|
|
365
|
+
/push.*external/, // external function can add
|
|
366
|
+
/users|addresses|recipients/i // named like user list
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
result.isUserControllable = userControllablePatterns.some(p => p.test(funcCode));
|
|
370
|
+
|
|
371
|
+
// Check for safe guards
|
|
372
|
+
const safeGuardPatterns = [
|
|
373
|
+
/require.*\.length\s*[<>=]/, // Length check in require
|
|
374
|
+
/Math\.min|min\s*\(/, // Min function for batching
|
|
375
|
+
/start.*end|offset.*limit/i, // Pagination parameters
|
|
376
|
+
/batch/i, // Batch processing
|
|
377
|
+
/MAX_|LIMIT_/, // Named limits
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
result.hasSafeGuards = safeGuardPatterns.some(p => p.test(funcCode));
|
|
381
|
+
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Check if code contains gas-heavy operations
|
|
387
|
+
*/
|
|
388
|
+
hasGasHeavyOperations(code) {
|
|
389
|
+
const gasHeavyPatterns = [
|
|
390
|
+
/\.transfer\s*\(/,
|
|
391
|
+
/\.call\s*\{/,
|
|
392
|
+
/\.call\s*\(/,
|
|
393
|
+
/\.send\s*\(/,
|
|
394
|
+
/\.delegatecall/,
|
|
395
|
+
/delete\s+\w+\[/, // Storage deletion
|
|
396
|
+
/\w+\[\w+\]\s*=/, // Storage write in loop
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
return gasHeavyPatterns.some(p => p.test(code));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
checkArrayPush(node) {
|
|
403
|
+
const funcCode = this.currentFunctionNode ?
|
|
404
|
+
this.getCodeSnippet(this.currentFunctionNode.loc) :
|
|
405
|
+
this.sourceLines.slice(Math.max(0, node.loc.start.line - 10), node.loc.start.line + 5).join('\n');
|
|
406
|
+
|
|
407
|
+
// Check for size limits
|
|
408
|
+
const hasLimit = /require[^;]*length\s*[<]=?\s*\d+|MAX_|LIMIT_/i.test(funcCode);
|
|
409
|
+
|
|
410
|
+
// Check if push is user-controllable
|
|
411
|
+
const isUserTriggered = /external|public/i.test(funcCode) &&
|
|
412
|
+
!(/onlyOwner|onlyAdmin|onlyRole/i.test(funcCode));
|
|
413
|
+
|
|
414
|
+
if (!hasLimit && isUserTriggered) {
|
|
415
|
+
// Check if array is ever iterated
|
|
416
|
+
const arrayName = this.findArrayName(node);
|
|
417
|
+
const isIterated = arrayName && new RegExp(`for[^}]*${arrayName}\\.length`).test(this.sourceCode);
|
|
418
|
+
|
|
419
|
+
if (isIterated) {
|
|
420
|
+
this.reportUnboundedArrayGrowth(node, arrayName);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
findArrayName(pushNode) {
|
|
426
|
+
if (pushNode.expression && pushNode.expression.expression) {
|
|
427
|
+
const expr = pushNode.expression.expression;
|
|
428
|
+
if (expr.type === 'Identifier') {
|
|
429
|
+
return expr.name;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
reportUnboundedIteration(node, analysis) {
|
|
436
|
+
this.addFinding({
|
|
437
|
+
title: 'Unbounded Loop Over User-Controlled Array',
|
|
438
|
+
description: `Loop iterates over '${analysis.arrayName}' array which can grow without bounds. If users can add elements via external calls, gas costs could exceed block limit causing permanent DoS.`,
|
|
439
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
440
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
441
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
442
|
+
code: this.getCodeSnippet(node.loc),
|
|
443
|
+
severity: 'HIGH',
|
|
444
|
+
confidence: 'HIGH',
|
|
445
|
+
exploitable: true,
|
|
446
|
+
exploitabilityScore: 80,
|
|
447
|
+
attackVector: 'gas-exhaustion-dos',
|
|
448
|
+
recommendation: 'Implement pagination (start/end indices), set maximum array size, or use pull payment pattern where users withdraw individually.',
|
|
449
|
+
references: [
|
|
450
|
+
'https://swcregistry.io/docs/SWC-128',
|
|
451
|
+
'https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/'
|
|
452
|
+
],
|
|
453
|
+
foundryPoC: this.generateDoSPoC(analysis.arrayName)
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
reportBoundedButExpensive(node, boundAnalysis) {
|
|
458
|
+
this.addFinding({
|
|
459
|
+
title: 'Gas-Expensive Loop',
|
|
460
|
+
description: `Loop has bound of ${boundAnalysis.boundValue} iterations with gas-heavy operations. While bounded, this could still be expensive and approach gas limits.`,
|
|
461
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
462
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
463
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
464
|
+
code: this.getCodeSnippet(node.loc),
|
|
465
|
+
severity: 'MEDIUM',
|
|
466
|
+
confidence: 'MEDIUM',
|
|
467
|
+
exploitable: false,
|
|
468
|
+
exploitabilityScore: 20,
|
|
469
|
+
recommendation: 'Consider reducing maximum iterations, batching operations, or using pull payment pattern for transfers.',
|
|
470
|
+
references: [
|
|
471
|
+
'https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/'
|
|
472
|
+
]
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
reportGasHeavyLoop(node) {
|
|
477
|
+
this.addFinding({
|
|
478
|
+
title: 'Gas-Heavy Operations in Unbounded Loop',
|
|
479
|
+
description: `External calls or storage operations inside a loop without explicit bounds. Each iteration costs significant gas, risking block gas limit exhaustion.`,
|
|
480
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
481
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
482
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
483
|
+
code: this.getCodeSnippet(node.loc),
|
|
484
|
+
severity: 'HIGH',
|
|
485
|
+
confidence: 'HIGH',
|
|
486
|
+
exploitable: true,
|
|
487
|
+
exploitabilityScore: 75,
|
|
488
|
+
attackVector: 'gas-exhaustion-dos',
|
|
489
|
+
recommendation: 'Add explicit iteration limits. Move external calls outside loops. Use pull payment pattern. Batch storage updates.',
|
|
490
|
+
references: [
|
|
491
|
+
'https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/'
|
|
492
|
+
]
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
reportExternalCallInLoop(node) {
|
|
497
|
+
this.addFinding({
|
|
498
|
+
title: 'External Call in Unbounded Loop',
|
|
499
|
+
description: `External call (.call, .transfer, .send) inside an unbounded loop. A single failing recipient can block all subsequent transfers (DoS), or gas costs could exceed block limit.`,
|
|
500
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
501
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
502
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
503
|
+
code: this.getCodeSnippet(node.loc),
|
|
504
|
+
severity: 'CRITICAL',
|
|
505
|
+
confidence: 'HIGH',
|
|
506
|
+
exploitable: true,
|
|
507
|
+
exploitabilityScore: 90,
|
|
508
|
+
attackVector: 'gas-exhaustion-dos',
|
|
509
|
+
recommendation: 'Use pull payment pattern (recipients withdraw individually). If push is required: add try/catch, continue on failure, and track failed transfers for retry.',
|
|
510
|
+
references: [
|
|
511
|
+
'https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/',
|
|
512
|
+
'https://docs.openzeppelin.com/contracts/4.x/api/security#PullPayment'
|
|
513
|
+
],
|
|
514
|
+
foundryPoC: this.generateExternalCallDoSPoC()
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
reportUnboundedWhileLoop(node) {
|
|
519
|
+
this.addFinding({
|
|
520
|
+
title: 'Unbounded While Loop with Gas-Heavy Operations',
|
|
521
|
+
description: `While loop without clear termination bounds contains operations that could exhaust gas. The loop may not terminate or could consume excessive gas.`,
|
|
522
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
523
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
524
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
525
|
+
code: this.getCodeSnippet(node.loc),
|
|
526
|
+
severity: 'HIGH',
|
|
527
|
+
confidence: 'MEDIUM',
|
|
528
|
+
exploitable: true,
|
|
529
|
+
exploitabilityScore: 65,
|
|
530
|
+
recommendation: 'Add explicit iteration counter with maximum limit. Consider converting to bounded for-loop. Add gas checks (gasleft()) if processing must continue across transactions.',
|
|
531
|
+
references: [
|
|
532
|
+
'https://swcregistry.io/docs/SWC-128'
|
|
533
|
+
]
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
reportPotentiallyUnboundedWhile(node) {
|
|
538
|
+
this.addFinding({
|
|
539
|
+
title: 'State-Processing While Loop',
|
|
540
|
+
description: `While loop processes state (queue/stack pattern) with gas-heavy operations. While logically bounded by state, could still exhaust gas with large state.`,
|
|
541
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
542
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
543
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
544
|
+
code: this.getCodeSnippet(node.loc),
|
|
545
|
+
severity: 'MEDIUM',
|
|
546
|
+
confidence: 'MEDIUM',
|
|
547
|
+
exploitable: true,
|
|
548
|
+
exploitabilityScore: 40,
|
|
549
|
+
recommendation: 'Add maximum iteration limit per transaction. Consider processing in batches across multiple transactions.',
|
|
550
|
+
references: [
|
|
551
|
+
'https://swcregistry.io/docs/SWC-128'
|
|
552
|
+
]
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
reportInfiniteLoop(node) {
|
|
557
|
+
this.addFinding({
|
|
558
|
+
title: 'Potential Infinite Loop',
|
|
559
|
+
description: `Loop with while(true) or for(;;) pattern detected. This will always consume all available gas unless explicitly broken.`,
|
|
560
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
561
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
562
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
563
|
+
code: this.getCodeSnippet(node.loc),
|
|
564
|
+
severity: 'CRITICAL',
|
|
565
|
+
confidence: 'HIGH',
|
|
566
|
+
exploitable: true,
|
|
567
|
+
exploitabilityScore: 95,
|
|
568
|
+
attackVector: 'infinite-loop-dos',
|
|
569
|
+
recommendation: 'Add explicit break conditions and maximum iteration counter. Verify all code paths lead to termination.',
|
|
570
|
+
references: [
|
|
571
|
+
'https://swcregistry.io/docs/SWC-128'
|
|
572
|
+
]
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
reportUnboundedArrayGrowth(node, arrayName) {
|
|
577
|
+
this.addFinding({
|
|
578
|
+
title: 'Unbounded Array Growth',
|
|
579
|
+
description: `Array '${arrayName || 'unknown'}' can grow without limits via user-accessible function, and is later iterated. Attackers can add elements until iteration exceeds block gas limit, causing permanent DoS.`,
|
|
580
|
+
location: `Contract: ${this.currentContract}, Function: ${this.currentFunction}`,
|
|
581
|
+
line: node.loc ? node.loc.start.line : 0,
|
|
582
|
+
column: node.loc ? node.loc.start.column : 0,
|
|
583
|
+
code: this.getCodeSnippet(node.loc),
|
|
584
|
+
severity: 'HIGH',
|
|
585
|
+
confidence: 'HIGH',
|
|
586
|
+
exploitable: true,
|
|
587
|
+
exploitabilityScore: 85,
|
|
588
|
+
attackVector: 'unbounded-growth-dos',
|
|
589
|
+
recommendation: 'Add maximum array size with require check. Use mapping with counter instead of array if iteration not needed. Implement removal mechanism.',
|
|
590
|
+
references: [
|
|
591
|
+
'https://swcregistry.io/docs/SWC-128'
|
|
592
|
+
],
|
|
593
|
+
foundryPoC: this.generateArrayGrowthPoC(arrayName)
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
generateDoSPoC(arrayName) {
|
|
598
|
+
return `// SPDX-License-Identifier: MIT
|
|
599
|
+
pragma solidity ^0.8.0;
|
|
600
|
+
|
|
601
|
+
import "forge-std/Test.sol";
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Proof of Concept: Gas Exhaustion DoS via Unbounded Loop
|
|
605
|
+
* Target array: ${arrayName || 'users'}
|
|
606
|
+
*/
|
|
607
|
+
contract GasExhaustionExploit is Test {
|
|
608
|
+
address constant TARGET = address(0);
|
|
609
|
+
|
|
610
|
+
function testExploit() public {
|
|
611
|
+
// Step 1: Grow the array to exceed gas limits
|
|
612
|
+
// Each push adds an element that will be iterated
|
|
613
|
+
for (uint i = 0; i < 10000; i++) {
|
|
614
|
+
// TARGET.addUser(address(uint160(i)));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Step 2: Attempt to call function that iterates
|
|
618
|
+
// This should fail with out-of-gas
|
|
619
|
+
// vm.expectRevert();
|
|
620
|
+
// TARGET.processAllUsers();
|
|
621
|
+
|
|
622
|
+
// The contract is now permanently DoS'd for this function
|
|
623
|
+
}
|
|
624
|
+
}`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
generateExternalCallDoSPoC() {
|
|
628
|
+
return `// SPDX-License-Identifier: MIT
|
|
629
|
+
pragma solidity ^0.8.0;
|
|
630
|
+
|
|
631
|
+
import "forge-std/Test.sol";
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Proof of Concept: DoS via Malicious Recipient in Loop
|
|
635
|
+
*/
|
|
636
|
+
contract MaliciousRecipient {
|
|
637
|
+
// Consume all gas or revert on receive
|
|
638
|
+
receive() external payable {
|
|
639
|
+
// Option 1: Infinite loop
|
|
640
|
+
while(true) {}
|
|
641
|
+
|
|
642
|
+
// Option 2: Simply revert
|
|
643
|
+
// revert("DoS");
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
contract ExternalCallLoopExploit is Test {
|
|
648
|
+
function testExploit() public {
|
|
649
|
+
// Deploy malicious recipient
|
|
650
|
+
MaliciousRecipient malicious = new MaliciousRecipient();
|
|
651
|
+
|
|
652
|
+
// Add malicious address to the payment queue
|
|
653
|
+
// TARGET.addRecipient(address(malicious));
|
|
654
|
+
|
|
655
|
+
// Now any call to distribute payments will fail
|
|
656
|
+
// Either consuming all gas or reverting
|
|
657
|
+
|
|
658
|
+
// vm.expectRevert();
|
|
659
|
+
// TARGET.distributePayments();
|
|
660
|
+
}
|
|
661
|
+
}`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
generateArrayGrowthPoC(arrayName) {
|
|
665
|
+
return `// SPDX-License-Identifier: MIT
|
|
666
|
+
pragma solidity ^0.8.0;
|
|
667
|
+
|
|
668
|
+
import "forge-std/Test.sol";
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Proof of Concept: Permanent DoS via Array Growth
|
|
672
|
+
* Target array: ${arrayName || 'array'}
|
|
673
|
+
*/
|
|
674
|
+
contract ArrayGrowthExploit is Test {
|
|
675
|
+
address constant TARGET = address(0);
|
|
676
|
+
|
|
677
|
+
function testExploit() public {
|
|
678
|
+
uint256 initialGas = gasleft();
|
|
679
|
+
|
|
680
|
+
// Keep adding elements until we approach gas limits
|
|
681
|
+
// Each element makes future iterations more expensive
|
|
682
|
+
|
|
683
|
+
uint256 elementsAdded = 0;
|
|
684
|
+
while (gasleft() > 100000) {
|
|
685
|
+
// TARGET.push(someValue);
|
|
686
|
+
elementsAdded++;
|
|
687
|
+
|
|
688
|
+
if (elementsAdded > 50000) break; // Safety limit for test
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
console.log("Elements added:", elementsAdded);
|
|
692
|
+
|
|
693
|
+
// Now try to call function that iterates over the array
|
|
694
|
+
// vm.expectRevert(); // Should fail with out-of-gas
|
|
695
|
+
// TARGET.processArray();
|
|
696
|
+
}
|
|
697
|
+
}`;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
module.exports = GasGriefingDetector;
|