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,409 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Signature Replay Attack Detector
5
+ * Detects vulnerabilities in contracts using off-chain signatures for meta-transactions
6
+ *
7
+ * Detects:
8
+ * - Missing nonce in signature verification
9
+ * - Reusable signatures without expiration
10
+ * - Missing chain ID in signature (cross-chain replay)
11
+ * - Weak signature verification (no EIP-712)
12
+ * - Missing signature validation checks
13
+ * - Replay protection bypasses
14
+ */
15
+ class SignatureReplayDetector extends BaseDetector {
16
+ constructor() {
17
+ super(
18
+ 'Signature Replay Vulnerability',
19
+ 'Detects missing replay protection in contracts using off-chain signatures (meta-transactions, permit, etc.)',
20
+ 'HIGH'
21
+ );
22
+ this.currentContract = null;
23
+ this.signatureFunctions = [];
24
+ this.nonceTracking = new Map(); // function -> has nonce
25
+ this.eip712Usage = false;
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
+ this.cfg = cfg;
35
+ this.dataFlow = dataFlow;
36
+
37
+ // Check for EIP-712 usage
38
+ this.eip712Usage = this.detectEIP712Usage(sourceCode);
39
+
40
+ this.traverse(ast);
41
+
42
+ // Post-traversal analysis
43
+ this.analyzeSignatureFunctions();
44
+
45
+ return this.findings;
46
+ }
47
+
48
+ visitContractDefinition(node) {
49
+ this.currentContract = node.name;
50
+ this.signatureFunctions = [];
51
+ this.nonceTracking = new Map();
52
+ }
53
+
54
+ visitFunctionDefinition(node) {
55
+ const funcName = node.name || '';
56
+ const funcCode = this.getCodeSnippet(node.loc);
57
+ const funcCodeLower = funcCode.toLowerCase();
58
+
59
+ // Detect signature verification functions
60
+ if (this.isSignatureFunction(funcCode, funcName)) {
61
+ const sigInfo = {
62
+ name: funcName,
63
+ node: node,
64
+ code: funcCode,
65
+ hasNonce: this.hasNonceProtection(funcCode),
66
+ hasExpiration: this.hasExpirationProtection(funcCode),
67
+ hasChainId: this.hasChainIdProtection(funcCode),
68
+ usesEIP712: this.usesEIP712InFunction(funcCode),
69
+ usesEcrecover: funcCodeLower.includes('ecrecover'),
70
+ usesSignature: funcCodeLower.includes('signature') || funcCodeLower.includes('sig')
71
+ };
72
+
73
+ this.signatureFunctions.push(sigInfo);
74
+ this.nonceTracking.set(funcName, sigInfo.hasNonce);
75
+
76
+ // Analyze this function for vulnerabilities
77
+ this.analyzeSignatureFunction(sigInfo);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Check if function uses signature verification
83
+ */
84
+ isSignatureFunction(code, funcName) {
85
+ const codeLower = code.toLowerCase();
86
+ const funcNameLower = funcName.toLowerCase();
87
+
88
+ // Common signature function patterns
89
+ const signaturePatterns = [
90
+ /ecrecover\s*\(/i,
91
+ /verify\s*\(.*signature/i,
92
+ /permit\s*\(/i,
93
+ /meta.*transaction/i,
94
+ /off.*chain.*sign/i,
95
+ /ECDSA\.recover/i,
96
+ /SignatureChecker/i,
97
+ /_verify/i,
98
+ /verifySignature/i,
99
+ /checkSignature/i
100
+ ];
101
+
102
+ // Function name patterns
103
+ const namePatterns = [
104
+ 'permit',
105
+ 'meta',
106
+ 'verify',
107
+ 'signature',
108
+ 'nonce'
109
+ ];
110
+
111
+ // IMPORTANT: do NOT classify generic "execute*" functions as signature-based.
112
+ // This prevents false positives on functions like executeFlashLoan(), execute(), etc.
113
+ const hasSignatureOps = signaturePatterns.some(pattern => pattern.test(code));
114
+ const nameIndicatesSig = namePatterns.some(pattern => funcNameLower.includes(pattern));
115
+
116
+ return hasSignatureOps || nameIndicatesSig;
117
+ }
118
+
119
+ /**
120
+ * Analyze a signature function for vulnerabilities
121
+ */
122
+ analyzeSignatureFunction(sigInfo) {
123
+ const { name, code, node, hasNonce, hasExpiration, hasChainId, usesEIP712, usesEcrecover } = sigInfo;
124
+
125
+ // Critical: Missing nonce protection
126
+ if (!hasNonce) {
127
+ this.addFinding({
128
+ title: 'Missing Nonce Protection in Signature Verification',
129
+ description: `Function '${name}' uses signature verification but lacks nonce tracking. Signatures can be replayed multiple times, allowing attackers to execute the same transaction repeatedly.`,
130
+ location: `Contract: ${this.currentContract}, Function: ${name}`,
131
+ line: node.loc ? node.loc.start.line : 0,
132
+ column: node.loc ? node.loc.start.column : 0,
133
+ code: this.getCodeSnippet(node.loc),
134
+ severity: 'CRITICAL',
135
+ confidence: 'HIGH',
136
+ exploitable: true,
137
+ exploitabilityScore: 90,
138
+ attackVector: 'signature-replay',
139
+ recommendation: 'Implement nonce tracking: mapping(address => uint256) public nonces; Increment nonce after each signature use. Include nonce in signature hash.',
140
+ references: [
141
+ 'https://swcregistry.io/docs/SWC-121',
142
+ 'https://eips.ethereum.org/EIPS/eip-2612',
143
+ 'https://docs.openzeppelin.com/contracts/4.x/api/utils#SignatureChecker'
144
+ ],
145
+ foundryPoC: this.generateNonceReplayPoC(this.currentContract, name)
146
+ });
147
+ }
148
+
149
+ // High: Missing expiration
150
+ if (!hasExpiration) {
151
+ this.addFinding({
152
+ title: 'Missing Expiration in Signature',
153
+ description: `Function '${name}' accepts signatures without expiration timestamp. Old signatures remain valid indefinitely, increasing attack surface.`,
154
+ location: `Contract: ${this.currentContract}, Function: ${name}`,
155
+ line: node.loc ? node.loc.start.line : 0,
156
+ column: node.loc ? node.loc.start.column : 0,
157
+ code: this.getCodeSnippet(node.loc),
158
+ severity: 'HIGH',
159
+ confidence: 'HIGH',
160
+ exploitable: true,
161
+ exploitabilityScore: 70,
162
+ attackVector: 'signature-replay',
163
+ recommendation: 'Include deadline/expiration timestamp in signature hash. Verify deadline > block.timestamp before processing.',
164
+ references: [
165
+ 'https://eips.ethereum.org/EIPS/eip-2612',
166
+ 'https://docs.openzeppelin.com/contracts/4.x/api/utils#SignatureChecker'
167
+ ]
168
+ });
169
+ }
170
+
171
+ // High: Missing chain ID (cross-chain replay)
172
+ if (!hasChainId && !this.eip712Usage) {
173
+ this.addFinding({
174
+ title: 'Missing Chain ID in Signature (Cross-Chain Replay Risk)',
175
+ description: `Function '${name}' does not include chain ID in signature verification. Signatures valid on one chain can be replayed on another chain (e.g., mainnet signature used on testnet).`,
176
+ location: `Contract: ${this.currentContract}, Function: ${name}`,
177
+ line: node.loc ? node.loc.start.line : 0,
178
+ column: node.loc ? node.loc.start.column : 0,
179
+ code: this.getCodeSnippet(node.loc),
180
+ severity: 'HIGH',
181
+ confidence: 'MEDIUM',
182
+ exploitable: true,
183
+ exploitabilityScore: 75,
184
+ attackVector: 'cross-chain-replay',
185
+ recommendation: 'Include chain ID in signature hash. Use EIP-712 for structured data signing which includes domain separator with chain ID.',
186
+ references: [
187
+ 'https://eips.ethereum.org/EIPS/eip-712',
188
+ 'https://eips.ethereum.org/EIPS/eip-2612'
189
+ ]
190
+ });
191
+ }
192
+
193
+ // Medium: Using ecrecover directly (not EIP-712)
194
+ if (usesEcrecover && !usesEIP712) {
195
+ this.addFinding({
196
+ title: 'Direct ecrecover Usage Without EIP-712',
197
+ description: `Function '${name}' uses ecrecover directly instead of EIP-712 structured data signing. This is error-prone and lacks type safety. Malformed signatures may be accepted.`,
198
+ location: `Contract: ${this.currentContract}, Function: ${name}`,
199
+ line: node.loc ? node.loc.start.line : 0,
200
+ column: node.loc ? node.loc.start.column : 0,
201
+ code: this.getCodeSnippet(node.loc),
202
+ severity: 'MEDIUM',
203
+ confidence: 'MEDIUM',
204
+ exploitable: false,
205
+ exploitabilityScore: 40,
206
+ attackVector: 'signature-verification',
207
+ recommendation: 'Use EIP-712 for structured data signing. Consider OpenZeppelin\'s SignatureChecker or ECDSA library for safer signature verification.',
208
+ references: [
209
+ 'https://eips.ethereum.org/EIPS/eip-712',
210
+ 'https://docs.openzeppelin.com/contracts/4.x/api/utils#ECDSA'
211
+ ]
212
+ });
213
+ }
214
+
215
+ // Check for weak signature validation
216
+ if (this.hasWeakSignatureValidation(code)) {
217
+ this.addFinding({
218
+ title: 'Weak Signature Validation',
219
+ description: `Function '${name}' has insufficient signature validation. Missing checks for zero address, signature malleability, or invalid signature format.`,
220
+ location: `Contract: ${this.currentContract}, Function: ${name}`,
221
+ line: node.loc ? node.loc.start.line : 0,
222
+ column: node.loc ? node.loc.start.column : 0,
223
+ code: this.getCodeSnippet(node.loc),
224
+ severity: 'MEDIUM',
225
+ confidence: 'MEDIUM',
226
+ exploitable: true,
227
+ exploitabilityScore: 60,
228
+ attackVector: 'signature-validation',
229
+ recommendation: 'Validate recovered address is not zero. Check signature length (65 bytes). Use OpenZeppelin\'s ECDSA.recover which handles malleability.',
230
+ references: [
231
+ 'https://docs.openzeppelin.com/contracts/4.x/api/utils#ECDSA',
232
+ 'https://swcregistry.io/docs/SWC-117'
233
+ ]
234
+ });
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Check if function has nonce protection
240
+ */
241
+ hasNonceProtection(code) {
242
+ const codeLower = code.toLowerCase();
243
+
244
+ // Check for nonce usage
245
+ const noncePatterns = [
246
+ /nonces\s*\[/i,
247
+ /nonce\s*\+\+/i,
248
+ /nonce\s*\+=\s*1/i,
249
+ /_useNonce\s*\(/i,
250
+ /incrementNonce/i,
251
+ /nonce.*require/i
252
+ ];
253
+
254
+ return noncePatterns.some(pattern => pattern.test(code));
255
+ }
256
+
257
+ /**
258
+ * Check if function has expiration protection
259
+ */
260
+ hasExpirationProtection(code) {
261
+ const codeLower = code.toLowerCase();
262
+
263
+ const expirationPatterns = [
264
+ /deadline\s*>/i,
265
+ /expires?\s*>/i,
266
+ /expiration\s*>/i,
267
+ /require\s*\(\s*.*deadline/i,
268
+ /require\s*\(\s*.*expir/i,
269
+ /block\.timestamp\s*</i
270
+ ];
271
+
272
+ return expirationPatterns.some(pattern => pattern.test(code));
273
+ }
274
+
275
+ /**
276
+ * Check if function includes chain ID
277
+ */
278
+ hasChainIdProtection(code) {
279
+ const codeLower = code.toLowerCase();
280
+
281
+ const chainIdPatterns = [
282
+ /chainid/i,
283
+ /chain\.id/i,
284
+ /getChainId/i,
285
+ /DOMAIN_SEPARATOR/i,
286
+ /eip712domain/i,
287
+ /typeHash.*chainId/i
288
+ ];
289
+
290
+ return chainIdPatterns.some(pattern => pattern.test(code));
291
+ }
292
+
293
+ /**
294
+ * Check if function uses EIP-712
295
+ */
296
+ usesEIP712InFunction(code) {
297
+ const codeLower = code.toLowerCase();
298
+
299
+ return /eip712|DOMAIN_SEPARATOR|_TYPE_HASH|_HASHED_NAME|_HASHED_VERSION/i.test(codeLower);
300
+ }
301
+
302
+ /**
303
+ * Check if contract uses EIP-712 overall
304
+ */
305
+ detectEIP712Usage(sourceCode) {
306
+ const codeLower = sourceCode.toLowerCase();
307
+ return /eip712|DOMAIN_SEPARATOR|_TYPE_HASH/i.test(codeLower);
308
+ }
309
+
310
+ /**
311
+ * Check for weak signature validation
312
+ */
313
+ hasWeakSignatureValidation(code) {
314
+ const codeLower = code.toLowerCase();
315
+
316
+ // Should check for zero address
317
+ const hasZeroCheck = /require\s*\(\s*.*!=\s*address\(0\)|require\s*\(\s*.*!=\s*0x0/i.test(codeLower);
318
+
319
+ // Should check signature length
320
+ const hasLengthCheck = /length\s*==\s*65|length\s*==\s*64/i.test(codeLower);
321
+
322
+ // Using ecrecover without proper validation
323
+ const usesEcrecover = /ecrecover\s*\(/i.test(codeLower);
324
+
325
+ return usesEcrecover && (!hasZeroCheck || !hasLengthCheck);
326
+ }
327
+
328
+ /**
329
+ * Post-traversal analysis
330
+ */
331
+ analyzeSignatureFunctions() {
332
+ // Check if contract has nonce mapping but functions don't use it
333
+ const hasNonceMapping = /mapping\s*\(.*\)\s*public\s*nonces/i.test(this.sourceCode);
334
+
335
+ if (hasNonceMapping) {
336
+ // Check if any signature function doesn't use it
337
+ this.signatureFunctions.forEach(sigInfo => {
338
+ if (!sigInfo.hasNonce) {
339
+ this.addFinding({
340
+ title: 'Nonce Mapping Exists But Not Used',
341
+ description: `Contract has nonce mapping but function '${sigInfo.name}' does not use it. This suggests incomplete implementation of replay protection.`,
342
+ location: `Contract: ${this.currentContract}, Function: ${sigInfo.name}`,
343
+ line: sigInfo.node.loc ? sigInfo.node.loc.start.line : 0,
344
+ column: sigInfo.node.loc ? sigInfo.node.loc.start.column : 0,
345
+ code: this.getCodeSnippet(sigInfo.node.loc),
346
+ severity: 'HIGH',
347
+ confidence: 'HIGH',
348
+ exploitable: true,
349
+ exploitabilityScore: 80,
350
+ attackVector: 'signature-replay',
351
+ recommendation: 'Ensure all signature verification functions increment and check nonces. Use _useNonce(address) pattern from OpenZeppelin.',
352
+ references: [
353
+ 'https://docs.openzeppelin.com/contracts/4.x/api/utils#SignatureChecker'
354
+ ]
355
+ });
356
+ }
357
+ });
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Generate Foundry PoC for nonce replay attack
363
+ */
364
+ generateNonceReplayPoC(contractName, funcName) {
365
+ return `// SPDX-License-Identifier: MIT
366
+ pragma solidity ^0.8.0;
367
+
368
+ import "forge-std/Test.sol";
369
+
370
+ /**
371
+ * Proof of Concept: Signature Replay Attack
372
+ * Target: ${contractName}.${funcName}()
373
+ * Attack Vector: Reuse same signature multiple times
374
+ */
375
+ contract SignatureReplayExploit is Test {
376
+ address constant TARGET = address(0); // ${contractName} address
377
+ address attacker = address(this);
378
+
379
+ uint256 privateKey = 0x1234...; // Attacker's private key
380
+ address signer = vm.addr(privateKey);
381
+
382
+ function testExploit() public {
383
+ // 1. Create signature for legitimate transaction
384
+ // bytes memory sig = signTransaction(...);
385
+
386
+ // 2. Execute transaction with signature (first time - legitimate)
387
+ // ${contractName}(TARGET).${funcName}(..., sig);
388
+
389
+ // 3. Replay the same signature (should fail but doesn't)
390
+ // ${contractName}(TARGET).${funcName}(..., sig); // Replay!
391
+
392
+ // 4. If nonce not checked, this succeeds and attacker benefits twice
393
+
394
+ // Assert replay succeeded
395
+ // assertGt(attackerBalance, expectedBalance);
396
+ }
397
+
398
+ // Helper: Sign transaction
399
+ // function signTransaction(...) internal returns (bytes memory) {
400
+ // bytes32 hash = keccak256(abi.encodePacked(...));
401
+ // (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, hash);
402
+ // return abi.encodePacked(r, s, v);
403
+ // }
404
+ }`;
405
+ }
406
+ }
407
+
408
+ module.exports = SignatureReplayDetector;
409
+