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,720 @@
|
|
|
1
|
+
const parser = require('@solidity-parser/parser');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advanced Data Flow Analyzer
|
|
5
|
+
* Implements real taint propagation with source-sink analysis
|
|
6
|
+
* Tracks user-controlled data through complex expressions and assignments
|
|
7
|
+
*/
|
|
8
|
+
class DataFlowAnalyzer {
|
|
9
|
+
constructor(controlFlowGraph) {
|
|
10
|
+
this.cfg = controlFlowGraph;
|
|
11
|
+
this.taintedVariables = new Map(); // variable -> taint info
|
|
12
|
+
this.taintedExpressions = new Map(); // expression hash -> taint info
|
|
13
|
+
this.dataFlows = [];
|
|
14
|
+
this.stateVariableTaints = new Map(); // state var -> taint info
|
|
15
|
+
this.arithmeticOperations = []; // Track arithmetic for precision loss
|
|
16
|
+
this.valueFlows = []; // Track ETH/token value flows
|
|
17
|
+
this.oracleValueFlows = []; // Track oracle-derived values flowing into value-moving operations
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Perform comprehensive taint analysis
|
|
22
|
+
*/
|
|
23
|
+
analyze() {
|
|
24
|
+
this.taintedVariables.clear();
|
|
25
|
+
this.taintedExpressions.clear();
|
|
26
|
+
this.dataFlows = [];
|
|
27
|
+
this.stateVariableTaints.clear();
|
|
28
|
+
this.arithmeticOperations = [];
|
|
29
|
+
this.valueFlows = [];
|
|
30
|
+
this.oracleValueFlows = [];
|
|
31
|
+
|
|
32
|
+
// Phase 1: Identify all taint sources
|
|
33
|
+
this.identifyTaintSources();
|
|
34
|
+
|
|
35
|
+
// Phase 2: Propagate taint through assignments (fixed-point)
|
|
36
|
+
this.propagateTaint();
|
|
37
|
+
|
|
38
|
+
// Phase 3: Track arithmetic operations for precision loss
|
|
39
|
+
this.analyzeArithmeticOperations();
|
|
40
|
+
|
|
41
|
+
// Phase 4: Track value flows (ETH, tokens)
|
|
42
|
+
this.analyzeValueFlows();
|
|
43
|
+
|
|
44
|
+
// Phase 5: Check dangerous sinks
|
|
45
|
+
this.checkDangerousSinks();
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
taintedVariables: this.taintedVariables,
|
|
49
|
+
stateVariableTaints: this.stateVariableTaints,
|
|
50
|
+
dataFlows: this.dataFlows,
|
|
51
|
+
arithmeticOperations: this.arithmeticOperations,
|
|
52
|
+
valueFlows: this.valueFlows,
|
|
53
|
+
oracleValueFlows: this.oracleValueFlows
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Identify all sources of tainted (user-controlled) data
|
|
59
|
+
*/
|
|
60
|
+
identifyTaintSources() {
|
|
61
|
+
for (const [funcKey, funcInfo] of this.cfg.functions) {
|
|
62
|
+
// Skip internal/private functions for external taint sources
|
|
63
|
+
const isExternallyCallable = funcInfo.visibility === 'public' ||
|
|
64
|
+
funcInfo.visibility === 'external' ||
|
|
65
|
+
funcInfo.isConstructor;
|
|
66
|
+
|
|
67
|
+
if (!isExternallyCallable && !funcInfo.isFallback && !funcInfo.isReceive) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Function parameters are tainted (user-controlled)
|
|
72
|
+
funcInfo.parameters.forEach((param, index) => {
|
|
73
|
+
if (param.name) {
|
|
74
|
+
const varKey = `${funcKey}.${param.name}`;
|
|
75
|
+
this.taintedVariables.set(varKey, {
|
|
76
|
+
type: 'parameter',
|
|
77
|
+
source: 'user_input',
|
|
78
|
+
function: funcKey,
|
|
79
|
+
name: param.name,
|
|
80
|
+
paramIndex: index,
|
|
81
|
+
paramType: param.type,
|
|
82
|
+
confidence: 'HIGH',
|
|
83
|
+
exploitability: this.assessParameterExploitability(param.type)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Analyze function body for msg.*, tx.*, block.* usage
|
|
89
|
+
if (funcInfo.node && funcInfo.node.body) {
|
|
90
|
+
this.findImplicitTaintSources(funcInfo.node.body, funcKey);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Find implicit taint sources in function body (msg.sender, msg.value, etc.)
|
|
97
|
+
*/
|
|
98
|
+
findImplicitTaintSources(node, funcKey) {
|
|
99
|
+
if (!node) return;
|
|
100
|
+
|
|
101
|
+
const self = this;
|
|
102
|
+
|
|
103
|
+
parser.visit(node, {
|
|
104
|
+
MemberAccess(memberNode) {
|
|
105
|
+
const expr = self.nodeToString(memberNode);
|
|
106
|
+
|
|
107
|
+
// msg.* sources
|
|
108
|
+
if (expr.startsWith('msg.')) {
|
|
109
|
+
self.taintedExpressions.set(expr, {
|
|
110
|
+
type: 'msg_property',
|
|
111
|
+
source: expr,
|
|
112
|
+
function: funcKey,
|
|
113
|
+
confidence: 'HIGH',
|
|
114
|
+
exploitability: expr === 'msg.value' ? 'HIGH' : 'MEDIUM'
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// tx.* sources
|
|
119
|
+
if (expr.startsWith('tx.')) {
|
|
120
|
+
self.taintedExpressions.set(expr, {
|
|
121
|
+
type: 'tx_property',
|
|
122
|
+
source: expr,
|
|
123
|
+
function: funcKey,
|
|
124
|
+
confidence: 'HIGH',
|
|
125
|
+
exploitability: expr === 'tx.origin' ? 'HIGH' : 'MEDIUM'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// block.* sources (manipulable by miners)
|
|
130
|
+
if (expr.startsWith('block.')) {
|
|
131
|
+
const manipulable = ['block.timestamp', 'block.number', 'block.coinbase'];
|
|
132
|
+
if (manipulable.some(m => expr.includes(m))) {
|
|
133
|
+
self.taintedExpressions.set(expr, {
|
|
134
|
+
type: 'block_property',
|
|
135
|
+
source: expr,
|
|
136
|
+
function: funcKey,
|
|
137
|
+
confidence: 'MEDIUM',
|
|
138
|
+
exploitability: 'MEDIUM',
|
|
139
|
+
note: 'Manipulable by miners/validators'
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Detect whether a function call expression looks like an oracle read.
|
|
149
|
+
* This is intentionally conservative and used to taint *oracle-derived values*,
|
|
150
|
+
* which can enable oracle->profit/dataflow checks.
|
|
151
|
+
*/
|
|
152
|
+
isOracleRead(node) {
|
|
153
|
+
if (!node) return false;
|
|
154
|
+
// Covers common Chainlink, Uniswap, and "median price" style oracles
|
|
155
|
+
const expr = this.nodeToString(node);
|
|
156
|
+
const oraclePatterns = [
|
|
157
|
+
/latestRoundData\s*\(/i,
|
|
158
|
+
/latestAnswer\s*\(/i,
|
|
159
|
+
/\.getReserves\s*\(\s*\)/i,
|
|
160
|
+
/\.slot0\s*\(\s*\)/i,
|
|
161
|
+
/\.observe\s*\(/i,
|
|
162
|
+
/getMedianPrice\s*\(/i,
|
|
163
|
+
/consult\s*\(/i,
|
|
164
|
+
/priceCumulative/i,
|
|
165
|
+
/cumulativePrice/i
|
|
166
|
+
];
|
|
167
|
+
return oraclePatterns.some(p => p.test(expr));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Propagate taint through assignments using fixed-point iteration
|
|
172
|
+
*/
|
|
173
|
+
propagateTaint() {
|
|
174
|
+
let changed = true;
|
|
175
|
+
let iterations = 0;
|
|
176
|
+
const maxIterations = 50;
|
|
177
|
+
|
|
178
|
+
while (changed && iterations < maxIterations) {
|
|
179
|
+
changed = false;
|
|
180
|
+
iterations++;
|
|
181
|
+
|
|
182
|
+
for (const [funcKey, funcInfo] of this.cfg.functions) {
|
|
183
|
+
if (!funcInfo.node || !funcInfo.node.body) continue;
|
|
184
|
+
|
|
185
|
+
// Analyze all assignments in function
|
|
186
|
+
const newTaints = this.analyzeAssignments(funcInfo.node.body, funcKey);
|
|
187
|
+
|
|
188
|
+
for (const [varKey, taintInfo] of newTaints) {
|
|
189
|
+
if (!this.taintedVariables.has(varKey)) {
|
|
190
|
+
this.taintedVariables.set(varKey, taintInfo);
|
|
191
|
+
changed = true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Propagate to state variables
|
|
196
|
+
funcInfo.stateWrites.forEach(write => {
|
|
197
|
+
const localTaint = this.findTaintForWrite(write, funcKey);
|
|
198
|
+
if (localTaint) {
|
|
199
|
+
const stateKey = `${funcInfo.contract}.${write.variable}`;
|
|
200
|
+
if (!this.stateVariableTaints.has(stateKey)) {
|
|
201
|
+
this.stateVariableTaints.set(stateKey, {
|
|
202
|
+
...localTaint,
|
|
203
|
+
stateVariable: write.variable,
|
|
204
|
+
writtenBy: funcKey,
|
|
205
|
+
loc: write.loc
|
|
206
|
+
});
|
|
207
|
+
changed = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Analyze assignments to propagate taint
|
|
217
|
+
*/
|
|
218
|
+
analyzeAssignments(node, funcKey) {
|
|
219
|
+
const newTaints = new Map();
|
|
220
|
+
const self = this;
|
|
221
|
+
|
|
222
|
+
parser.visit(node, {
|
|
223
|
+
BinaryOperation(binNode) {
|
|
224
|
+
if (!['=', '+=', '-=', '*=', '/='].includes(binNode.operator)) return;
|
|
225
|
+
|
|
226
|
+
const leftName = self.getAssignmentTarget(binNode.left);
|
|
227
|
+
if (!leftName) return;
|
|
228
|
+
|
|
229
|
+
const rightExpr = self.nodeToString(binNode.right);
|
|
230
|
+
const rightTaint = self.isExpressionTainted(binNode.right, funcKey);
|
|
231
|
+
|
|
232
|
+
if (rightTaint) {
|
|
233
|
+
const varKey = `${funcKey}.${leftName}`;
|
|
234
|
+
newTaints.set(varKey, {
|
|
235
|
+
type: 'derived',
|
|
236
|
+
source: rightTaint.source,
|
|
237
|
+
derivedFrom: rightExpr,
|
|
238
|
+
function: funcKey,
|
|
239
|
+
name: leftName,
|
|
240
|
+
confidence: rightTaint.confidence,
|
|
241
|
+
exploitability: rightTaint.exploitability
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
VariableDeclarationStatement(declNode) {
|
|
247
|
+
if (!declNode.initialValue) return;
|
|
248
|
+
|
|
249
|
+
declNode.variables.forEach(variable => {
|
|
250
|
+
if (!variable || !variable.name) return;
|
|
251
|
+
|
|
252
|
+
const rightTaint = self.isExpressionTainted(declNode.initialValue, funcKey);
|
|
253
|
+
if (rightTaint) {
|
|
254
|
+
const varKey = `${funcKey}.${variable.name}`;
|
|
255
|
+
newTaints.set(varKey, {
|
|
256
|
+
type: 'derived',
|
|
257
|
+
source: rightTaint.source,
|
|
258
|
+
derivedFrom: self.nodeToString(declNode.initialValue),
|
|
259
|
+
function: funcKey,
|
|
260
|
+
name: variable.name,
|
|
261
|
+
confidence: rightTaint.confidence,
|
|
262
|
+
exploitability: rightTaint.exploitability
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return newTaints;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Check if an expression is tainted
|
|
274
|
+
*/
|
|
275
|
+
isExpressionTainted(node, funcKey) {
|
|
276
|
+
if (!node) return null;
|
|
277
|
+
|
|
278
|
+
const exprStr = this.nodeToString(node);
|
|
279
|
+
|
|
280
|
+
// Oracle read is a taint source (even if no user input involved)
|
|
281
|
+
if (node.type === 'FunctionCall' && this.isOracleRead(node)) {
|
|
282
|
+
return {
|
|
283
|
+
type: 'oracle_read',
|
|
284
|
+
source: exprStr,
|
|
285
|
+
confidence: 'HIGH',
|
|
286
|
+
exploitability: 'HIGH'
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Direct taint source
|
|
291
|
+
if (this.taintedExpressions.has(exprStr)) {
|
|
292
|
+
return this.taintedExpressions.get(exprStr);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check if it's a tainted variable
|
|
296
|
+
if (node.type === 'Identifier') {
|
|
297
|
+
const varKey = `${funcKey}.${node.name}`;
|
|
298
|
+
if (this.taintedVariables.has(varKey)) {
|
|
299
|
+
return this.taintedVariables.get(varKey);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check for msg.*, tx.*, block.* in expression
|
|
304
|
+
if (exprStr.includes('msg.') || exprStr.includes('tx.') || exprStr.includes('block.timestamp')) {
|
|
305
|
+
return {
|
|
306
|
+
type: 'implicit',
|
|
307
|
+
source: exprStr,
|
|
308
|
+
confidence: 'MEDIUM',
|
|
309
|
+
exploitability: 'MEDIUM'
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Recursively check sub-expressions
|
|
314
|
+
if (node.type === 'BinaryOperation') {
|
|
315
|
+
const leftTaint = this.isExpressionTainted(node.left, funcKey);
|
|
316
|
+
const rightTaint = this.isExpressionTainted(node.right, funcKey);
|
|
317
|
+
return leftTaint || rightTaint;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (node.type === 'FunctionCall' && node.arguments) {
|
|
321
|
+
// Also consider oracle reads on the callee expression (e.g., oracle.latestRoundData())
|
|
322
|
+
if (this.isOracleRead(node)) {
|
|
323
|
+
return {
|
|
324
|
+
type: 'oracle_read',
|
|
325
|
+
source: exprStr,
|
|
326
|
+
confidence: 'HIGH',
|
|
327
|
+
exploitability: 'HIGH'
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
for (const arg of node.arguments) {
|
|
331
|
+
const argTaint = this.isExpressionTainted(arg, funcKey);
|
|
332
|
+
if (argTaint) return argTaint;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (node.type === 'IndexAccess') {
|
|
337
|
+
const baseTaint = this.isExpressionTainted(node.base, funcKey);
|
|
338
|
+
const indexTaint = this.isExpressionTainted(node.index, funcKey);
|
|
339
|
+
return baseTaint || indexTaint;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (node.type === 'MemberAccess') {
|
|
343
|
+
return this.isExpressionTainted(node.expression, funcKey);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Analyze arithmetic operations for precision loss vulnerabilities
|
|
351
|
+
*/
|
|
352
|
+
analyzeArithmeticOperations() {
|
|
353
|
+
for (const [funcKey, funcInfo] of this.cfg.functions) {
|
|
354
|
+
if (!funcInfo.node || !funcInfo.node.body) continue;
|
|
355
|
+
|
|
356
|
+
const self = this;
|
|
357
|
+
|
|
358
|
+
parser.visit(funcInfo.node.body, {
|
|
359
|
+
BinaryOperation(node) {
|
|
360
|
+
if (!['+', '-', '*', '/', '%'].includes(node.operator)) return;
|
|
361
|
+
|
|
362
|
+
const operation = {
|
|
363
|
+
function: funcKey,
|
|
364
|
+
operator: node.operator,
|
|
365
|
+
left: self.nodeToString(node.left),
|
|
366
|
+
right: self.nodeToString(node.right),
|
|
367
|
+
loc: node.loc,
|
|
368
|
+
issues: []
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Check for division before multiplication (precision loss)
|
|
372
|
+
if (node.operator === '*') {
|
|
373
|
+
if (self.containsDivision(node.left) || self.containsDivision(node.right)) {
|
|
374
|
+
operation.issues.push({
|
|
375
|
+
type: 'division_before_multiplication',
|
|
376
|
+
severity: 'MEDIUM',
|
|
377
|
+
description: 'Division before multiplication can cause precision loss'
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Check for division that could truncate to zero
|
|
383
|
+
if (node.operator === '/') {
|
|
384
|
+
const rightVal = self.tryGetConstantValue(node.right);
|
|
385
|
+
if (rightVal && rightVal > 1e18) {
|
|
386
|
+
operation.issues.push({
|
|
387
|
+
type: 'large_divisor',
|
|
388
|
+
severity: 'MEDIUM',
|
|
389
|
+
description: 'Division by large number may truncate to zero'
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Check for unchecked arithmetic with tainted values
|
|
395
|
+
const leftTaint = self.isExpressionTainted(node.left, funcKey);
|
|
396
|
+
const rightTaint = self.isExpressionTainted(node.right, funcKey);
|
|
397
|
+
|
|
398
|
+
if (leftTaint || rightTaint) {
|
|
399
|
+
operation.tainted = true;
|
|
400
|
+
operation.taintSource = (leftTaint || rightTaint).source;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (operation.issues.length > 0 || operation.tainted) {
|
|
404
|
+
self.arithmeticOperations.push(operation);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Analyze value flows (ETH and token transfers)
|
|
413
|
+
*/
|
|
414
|
+
analyzeValueFlows() {
|
|
415
|
+
for (const [funcKey, funcInfo] of this.cfg.functions) {
|
|
416
|
+
// Check external calls for value transfers
|
|
417
|
+
funcInfo.externalCalls.forEach(call => {
|
|
418
|
+
if (['call', 'transfer', 'send'].includes(call.type)) {
|
|
419
|
+
const flow = {
|
|
420
|
+
function: funcKey,
|
|
421
|
+
type: call.type,
|
|
422
|
+
target: call.target,
|
|
423
|
+
loc: call.loc,
|
|
424
|
+
issues: []
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// Check if target is tainted (user-controlled recipient)
|
|
428
|
+
const targetTaint = this.isExpressionTainted({ type: 'Identifier', name: call.target }, funcKey);
|
|
429
|
+
if (targetTaint || call.target.includes('msg.sender')) {
|
|
430
|
+
flow.targetTainted = true;
|
|
431
|
+
flow.targetTaintSource = targetTaint?.source || 'msg.sender';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Check if this is in a loop (gas griefing potential)
|
|
435
|
+
// (would need loop context tracking)
|
|
436
|
+
|
|
437
|
+
this.valueFlows.push(flow);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Additionally: detect oracle-derived values used as amounts in value-moving calls.
|
|
442
|
+
// This enables oracle manipulation detectors to reduce pure-regex false positives.
|
|
443
|
+
if (!funcInfo.node || !funcInfo.node.body) continue;
|
|
444
|
+
const self = this;
|
|
445
|
+
|
|
446
|
+
parser.visit(funcInfo.node.body, {
|
|
447
|
+
FunctionCall(node) {
|
|
448
|
+
// Identify value-moving calls: transfer(to, amount), transferFrom(from,to,amount), mint(to,amount), burn(amount)
|
|
449
|
+
const callee = self.nodeToString(node.expression).toLowerCase();
|
|
450
|
+
if (!callee) return;
|
|
451
|
+
|
|
452
|
+
const isValueMoving =
|
|
453
|
+
/transferfrom\s*\(/.test(callee) ||
|
|
454
|
+
/transfer\s*\(/.test(callee) ||
|
|
455
|
+
/mint\s*\(/.test(callee) ||
|
|
456
|
+
/_mint\s*\(/.test(callee) ||
|
|
457
|
+
/burn\s*\(/.test(callee) ||
|
|
458
|
+
/_burn\s*\(/.test(callee);
|
|
459
|
+
|
|
460
|
+
if (!isValueMoving) return;
|
|
461
|
+
|
|
462
|
+
// Heuristic: last argument is commonly the amount
|
|
463
|
+
const args = node.arguments || [];
|
|
464
|
+
if (args.length === 0) return;
|
|
465
|
+
const amountNode = args[args.length - 1];
|
|
466
|
+
const amountExpr = self.nodeToString(amountNode);
|
|
467
|
+
const amountTaint = self.isExpressionTainted(amountNode, funcKey);
|
|
468
|
+
|
|
469
|
+
if (amountTaint && amountTaint.type === 'oracle_read') {
|
|
470
|
+
self.oracleValueFlows.push({
|
|
471
|
+
function: funcKey,
|
|
472
|
+
contract: funcInfo.contract,
|
|
473
|
+
operation: callee,
|
|
474
|
+
amountExpr,
|
|
475
|
+
oracleSource: amountTaint.source,
|
|
476
|
+
loc: node.loc
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Check if tainted data reaches dangerous sinks
|
|
486
|
+
*/
|
|
487
|
+
checkDangerousSinks() {
|
|
488
|
+
const dangerousSinks = {
|
|
489
|
+
'delegatecall': { severity: 'CRITICAL', description: 'Arbitrary code execution' },
|
|
490
|
+
'selfdestruct': { severity: 'CRITICAL', description: 'Contract destruction' },
|
|
491
|
+
'suicide': { severity: 'CRITICAL', description: 'Contract destruction (deprecated)' },
|
|
492
|
+
'call': { severity: 'HIGH', description: 'External call with potential reentrancy' },
|
|
493
|
+
'staticcall': { severity: 'MEDIUM', description: 'External read call' },
|
|
494
|
+
'send': { severity: 'HIGH', description: 'ETH transfer' }
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
for (const [funcKey, funcInfo] of this.cfg.functions) {
|
|
498
|
+
funcInfo.externalCalls.forEach(call => {
|
|
499
|
+
const sinkInfo = dangerousSinks[call.type];
|
|
500
|
+
if (!sinkInfo) return;
|
|
501
|
+
|
|
502
|
+
// Check if call target is tainted
|
|
503
|
+
const targetTaint = this.checkCallTargetTaint(call, funcKey);
|
|
504
|
+
|
|
505
|
+
if (targetTaint) {
|
|
506
|
+
const exploitable = this.isExploitable(funcKey, call);
|
|
507
|
+
|
|
508
|
+
this.dataFlows.push({
|
|
509
|
+
source: targetTaint.source,
|
|
510
|
+
sourceType: targetTaint.type,
|
|
511
|
+
sink: call.type,
|
|
512
|
+
sinkDescription: sinkInfo.description,
|
|
513
|
+
function: funcKey,
|
|
514
|
+
contract: funcInfo.contract,
|
|
515
|
+
location: call.loc,
|
|
516
|
+
severity: sinkInfo.severity,
|
|
517
|
+
confidence: targetTaint.confidence,
|
|
518
|
+
exploitable: exploitable,
|
|
519
|
+
exploitabilityReason: exploitable ?
|
|
520
|
+
'Function is externally callable without effective access control' :
|
|
521
|
+
'Protected by access control or not externally callable',
|
|
522
|
+
taintPath: this.reconstructTaintPath(targetTaint, call, funcKey)
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Check if call target is tainted
|
|
531
|
+
*/
|
|
532
|
+
checkCallTargetTaint(call, funcKey) {
|
|
533
|
+
// Check if target address is tainted
|
|
534
|
+
if (call.target) {
|
|
535
|
+
// Direct parameter taint
|
|
536
|
+
const varKey = `${funcKey}.${call.target}`;
|
|
537
|
+
if (this.taintedVariables.has(varKey)) {
|
|
538
|
+
return this.taintedVariables.get(varKey);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Check for msg.sender (always tainted)
|
|
542
|
+
if (call.target.includes('msg.sender')) {
|
|
543
|
+
return {
|
|
544
|
+
type: 'msg_property',
|
|
545
|
+
source: 'msg.sender',
|
|
546
|
+
confidence: 'HIGH'
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Check state variable taint
|
|
551
|
+
for (const [stateKey, taint] of this.stateVariableTaints) {
|
|
552
|
+
if (call.target.includes(stateKey.split('.')[1])) {
|
|
553
|
+
return taint;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Reconstruct the taint propagation path
|
|
563
|
+
*/
|
|
564
|
+
reconstructTaintPath(taint, call, funcKey) {
|
|
565
|
+
const path = [];
|
|
566
|
+
|
|
567
|
+
path.push({
|
|
568
|
+
step: 'source',
|
|
569
|
+
description: `Taint originates from ${taint.source}`,
|
|
570
|
+
type: taint.type
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
if (taint.derivedFrom) {
|
|
574
|
+
path.push({
|
|
575
|
+
step: 'propagation',
|
|
576
|
+
description: `Derived through: ${taint.derivedFrom}`,
|
|
577
|
+
type: 'assignment'
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
path.push({
|
|
582
|
+
step: 'sink',
|
|
583
|
+
description: `Reaches dangerous operation: ${call.type}`,
|
|
584
|
+
location: call.loc
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
return path;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Determine if a taint flow is exploitable
|
|
592
|
+
*/
|
|
593
|
+
isExploitable(funcKey, call) {
|
|
594
|
+
const funcInfo = this.cfg.functions.get(funcKey);
|
|
595
|
+
if (!funcInfo) return false;
|
|
596
|
+
|
|
597
|
+
// Private/internal functions not directly exploitable
|
|
598
|
+
if (funcInfo.visibility === 'private' || funcInfo.visibility === 'internal') {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Check for effective access control
|
|
603
|
+
if (funcInfo.modifiers.length > 0) {
|
|
604
|
+
for (const modName of funcInfo.modifiers) {
|
|
605
|
+
const modKey = `${funcInfo.contract}.${modName}`;
|
|
606
|
+
const modInfo = this.cfg.modifiers.get(modKey);
|
|
607
|
+
|
|
608
|
+
if (modInfo && (modInfo.checksAccess || modInfo.checksOwnership || modInfo.checksRole)) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Assess exploitability based on parameter type
|
|
619
|
+
*/
|
|
620
|
+
assessParameterExploitability(paramType) {
|
|
621
|
+
if (!paramType) return 'MEDIUM';
|
|
622
|
+
|
|
623
|
+
// Address parameters - high exploitability for target manipulation
|
|
624
|
+
if (paramType.includes('address')) return 'HIGH';
|
|
625
|
+
|
|
626
|
+
// Bytes parameters - can contain arbitrary data
|
|
627
|
+
if (paramType.includes('bytes')) return 'HIGH';
|
|
628
|
+
|
|
629
|
+
// Integer parameters - potential for overflow/manipulation
|
|
630
|
+
if (paramType.includes('uint') || paramType.includes('int')) return 'MEDIUM';
|
|
631
|
+
|
|
632
|
+
return 'MEDIUM';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Helper methods
|
|
636
|
+
|
|
637
|
+
nodeToString(node) {
|
|
638
|
+
if (!node) return '';
|
|
639
|
+
|
|
640
|
+
switch (node.type) {
|
|
641
|
+
case 'Identifier':
|
|
642
|
+
return node.name;
|
|
643
|
+
case 'MemberAccess':
|
|
644
|
+
return `${this.nodeToString(node.expression)}.${node.memberName}`;
|
|
645
|
+
case 'IndexAccess':
|
|
646
|
+
return `${this.nodeToString(node.base)}[${this.nodeToString(node.index)}]`;
|
|
647
|
+
case 'NumberLiteral':
|
|
648
|
+
return node.number;
|
|
649
|
+
case 'BinaryOperation':
|
|
650
|
+
return `${this.nodeToString(node.left)} ${node.operator} ${this.nodeToString(node.right)}`;
|
|
651
|
+
case 'FunctionCall':
|
|
652
|
+
const funcName = this.nodeToString(node.expression);
|
|
653
|
+
const args = (node.arguments || []).map(a => this.nodeToString(a)).join(', ');
|
|
654
|
+
return `${funcName}(${args})`;
|
|
655
|
+
default:
|
|
656
|
+
return `[${node.type}]`;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
getAssignmentTarget(node) {
|
|
661
|
+
if (!node) return null;
|
|
662
|
+
if (node.type === 'Identifier') return node.name;
|
|
663
|
+
if (node.type === 'IndexAccess') return this.nodeToString(node.base);
|
|
664
|
+
if (node.type === 'MemberAccess') return this.nodeToString(node);
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
findTaintForWrite(write, funcKey) {
|
|
669
|
+
// Check if any tainted variable could flow to this write
|
|
670
|
+
for (const [varKey, taint] of this.taintedVariables) {
|
|
671
|
+
if (varKey.startsWith(funcKey)) {
|
|
672
|
+
return taint;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
containsDivision(node) {
|
|
679
|
+
if (!node) return false;
|
|
680
|
+
if (node.type === 'BinaryOperation' && node.operator === '/') return true;
|
|
681
|
+
if (node.type === 'BinaryOperation') {
|
|
682
|
+
return this.containsDivision(node.left) || this.containsDivision(node.right);
|
|
683
|
+
}
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
tryGetConstantValue(node) {
|
|
688
|
+
if (!node) return null;
|
|
689
|
+
if (node.type === 'NumberLiteral') {
|
|
690
|
+
return parseFloat(node.number);
|
|
691
|
+
}
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Public API methods
|
|
696
|
+
|
|
697
|
+
isTainted(varName, funcKey) {
|
|
698
|
+
const varKey = `${funcKey}.${varName}`;
|
|
699
|
+
return this.taintedVariables.has(varKey);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
getTaintInfo(varName, funcKey) {
|
|
703
|
+
const varKey = `${funcKey}.${varName}`;
|
|
704
|
+
return this.taintedVariables.get(varKey);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
getDangerousFlows(funcKey) {
|
|
708
|
+
return this.dataFlows.filter(flow => flow.function === funcKey);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
getExploitableFlows() {
|
|
712
|
+
return this.dataFlows.filter(flow => flow.exploitable);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
getPrecisionLossRisks() {
|
|
716
|
+
return this.arithmeticOperations.filter(op => op.issues.length > 0);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
module.exports = DataFlowAnalyzer;
|