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,427 @@
|
|
|
1
|
+
const BaseDetector = require('./base-detector');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Time-of-Check to Time-of-Use (TOCTOU) Detector
|
|
5
|
+
* Detects race conditions where contract state changes between a check and its use
|
|
6
|
+
*
|
|
7
|
+
* Detects:
|
|
8
|
+
* - Balance checks before transfers
|
|
9
|
+
* - Allowance checks before transfers
|
|
10
|
+
* - State checks before state-dependent operations
|
|
11
|
+
* - External calls between check and use
|
|
12
|
+
* - Reentrancy-like patterns with state checks
|
|
13
|
+
*/
|
|
14
|
+
class TOCTOUDetector extends BaseDetector {
|
|
15
|
+
constructor() {
|
|
16
|
+
super(
|
|
17
|
+
'Time-of-Check to Time-of-Use (TOCTOU) Vulnerability',
|
|
18
|
+
'Detects race conditions where state changes between check and use',
|
|
19
|
+
'HIGH'
|
|
20
|
+
);
|
|
21
|
+
this.currentContract = null;
|
|
22
|
+
this.currentFunction = null;
|
|
23
|
+
this.checkUsePairs = []; // Array of {check, use, hasExternalCall}
|
|
24
|
+
this.cfg = null;
|
|
25
|
+
this.dataFlow = null;
|
|
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
|
+
this.traverse(ast);
|
|
38
|
+
|
|
39
|
+
// Post-traversal analysis
|
|
40
|
+
this.analyzeTOCTOUPatterns();
|
|
41
|
+
|
|
42
|
+
return this.findings;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
visitContractDefinition(node) {
|
|
46
|
+
this.currentContract = node.name;
|
|
47
|
+
this.checkUsePairs = [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
visitFunctionDefinition(node) {
|
|
51
|
+
this.currentFunction = node.name || '';
|
|
52
|
+
|
|
53
|
+
if (!node.body || !node.body.statements) return;
|
|
54
|
+
|
|
55
|
+
// Skip private/internal functions
|
|
56
|
+
if (node.visibility === 'private' || node.visibility === 'internal') {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Analyze function for TOCTOU patterns
|
|
61
|
+
this.analyzeFunctionForTOCTOU(node);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Analyze function for TOCTOU vulnerabilities
|
|
66
|
+
*/
|
|
67
|
+
analyzeFunctionForTOCTOU(node) {
|
|
68
|
+
if (!node.body || !node.body.statements) return;
|
|
69
|
+
|
|
70
|
+
const statements = node.body.statements;
|
|
71
|
+
const checks = []; // Array of check statements
|
|
72
|
+
const uses = []; // Array of use statements
|
|
73
|
+
const externalCalls = []; // Array of external calls
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < statements.length; i++) {
|
|
76
|
+
const stmt = statements[i];
|
|
77
|
+
const stmtCode = this.getCodeSnippet(stmt.loc);
|
|
78
|
+
const stmtCodeLower = stmtCode.toLowerCase();
|
|
79
|
+
|
|
80
|
+
// Detect check patterns
|
|
81
|
+
if (this.isCheckPattern(stmt, stmtCode)) {
|
|
82
|
+
checks.push({
|
|
83
|
+
statement: stmt,
|
|
84
|
+
code: stmtCode,
|
|
85
|
+
index: i,
|
|
86
|
+
type: this.getCheckType(stmtCode)
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Detect use patterns
|
|
91
|
+
if (this.isUsePattern(stmt, stmtCode)) {
|
|
92
|
+
uses.push({
|
|
93
|
+
statement: stmt,
|
|
94
|
+
code: stmtCode,
|
|
95
|
+
index: i,
|
|
96
|
+
type: this.getUseType(stmtCode)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Detect external calls
|
|
101
|
+
if (this.isExternalCall(stmt, stmtCode)) {
|
|
102
|
+
externalCalls.push({
|
|
103
|
+
statement: stmt,
|
|
104
|
+
code: stmtCode,
|
|
105
|
+
index: i
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Find check-use pairs with external calls in between
|
|
111
|
+
this.findTOCTOUPatterns(checks, uses, externalCalls, node);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if statement is a check pattern
|
|
116
|
+
*/
|
|
117
|
+
isCheckPattern(stmt, code) {
|
|
118
|
+
const codeLower = code.toLowerCase();
|
|
119
|
+
|
|
120
|
+
// Balance checks - look for balance reads/assignments that are used for checks
|
|
121
|
+
const balanceChecks = [
|
|
122
|
+
/balanceOf\s*\(/i,
|
|
123
|
+
/balances\[/i,
|
|
124
|
+
/\.balance\s*>/i,
|
|
125
|
+
/\.balance\s*>=/i,
|
|
126
|
+
/\.balance\s*</i,
|
|
127
|
+
/\.balance\s*<=/i,
|
|
128
|
+
/uint256\s+\w*balance/i, // uint256 balance = ...
|
|
129
|
+
/require\s*\(\s*.*balance/i,
|
|
130
|
+
/if\s*\(\s*.*balance/i
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// Allowance checks
|
|
134
|
+
const allowanceChecks = [
|
|
135
|
+
/allowance\s*\(/i,
|
|
136
|
+
/allowance\[/i,
|
|
137
|
+
/uint256\s+\w*allowed/i, // uint256 allowed = ...
|
|
138
|
+
/require\s*\(\s*.*allowance/i,
|
|
139
|
+
/if\s*\(\s*.*allowance/i
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
// State variable checks
|
|
143
|
+
const stateChecks = [
|
|
144
|
+
/require\s*\(\s*.*==/i,
|
|
145
|
+
/require\s*\(\s*.*!=/i,
|
|
146
|
+
/if\s*\(\s*.*==/i,
|
|
147
|
+
/if\s*\(\s*.*!=/i
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
// Owner/role checks
|
|
151
|
+
const accessChecks = [
|
|
152
|
+
/require\s*\(\s*.*owner/i,
|
|
153
|
+
/require\s*\(\s*.*role/i,
|
|
154
|
+
/require\s*\(\s*.*hasRole/i
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const allChecks = [...balanceChecks, ...allowanceChecks, ...stateChecks, ...accessChecks];
|
|
158
|
+
|
|
159
|
+
return allChecks.some(pattern => pattern.test(code));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get type of check
|
|
164
|
+
*/
|
|
165
|
+
getCheckType(code) {
|
|
166
|
+
const codeLower = code.toLowerCase();
|
|
167
|
+
// Check for balance reads (including variable assignments)
|
|
168
|
+
if (codeLower.includes('balance') || codeLower.includes('balances[')) return 'balance';
|
|
169
|
+
// Check for allowance reads
|
|
170
|
+
if (codeLower.includes('allowance') || codeLower.includes('allowed')) return 'allowance';
|
|
171
|
+
if (codeLower.includes('owner') || codeLower.includes('role')) return 'access';
|
|
172
|
+
return 'state';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if statement is a use pattern (state modification)
|
|
177
|
+
*/
|
|
178
|
+
isUsePattern(stmt, code) {
|
|
179
|
+
const codeLower = code.toLowerCase();
|
|
180
|
+
|
|
181
|
+
// State modifications - assignments to state variables
|
|
182
|
+
const stateModPatterns = [
|
|
183
|
+
/balances\[.*\]\s*=/i, // balances[user] = ...
|
|
184
|
+
/balances\[.*\]\s*-=/i, // balances[user] -= ...
|
|
185
|
+
/balances\[.*\]\s*\+=/i, // balances[user] += ...
|
|
186
|
+
/allowance\[.*\]\s*=/i, // allowance[from][spender] = ...
|
|
187
|
+
/allowance\[.*\]\s*-=/i, // allowance[from][spender] -= ...
|
|
188
|
+
/mapping\[.*\]\s*=/i, // mapping assignments
|
|
189
|
+
/\w+\s*=\s*[^=]/i, // Variable assignment (but not ==)
|
|
190
|
+
/\+\+/i, // Increment
|
|
191
|
+
/--/i // Decrement
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
// Check if it's an assignment statement
|
|
195
|
+
if (stmt.type === 'ExpressionStatement' && stmt.expression) {
|
|
196
|
+
const expr = stmt.expression;
|
|
197
|
+
if (expr.type === 'Assignment') {
|
|
198
|
+
// This is an assignment - check if it's a state variable
|
|
199
|
+
const leftSide = this.getCodeSnippet(expr.left ? expr.left.loc : null);
|
|
200
|
+
if (leftSide && (leftSide.includes('balances') || leftSide.includes('allowance') || leftSide.includes('mapping'))) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check patterns in code
|
|
207
|
+
return stateModPatterns.some(pattern => pattern.test(code));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get type of use
|
|
212
|
+
*/
|
|
213
|
+
getUseType(code) {
|
|
214
|
+
const codeLower = code.toLowerCase();
|
|
215
|
+
// Check for transfer operations
|
|
216
|
+
if (codeLower.includes('transfer') || codeLower.includes('call{value') || codeLower.includes('send')) return 'transfer';
|
|
217
|
+
// Check for state modifications to balances or allowance
|
|
218
|
+
if (codeLower.includes('balances[') && (codeLower.includes('=') || codeLower.includes('-=') || codeLower.includes('+='))) return 'transfer';
|
|
219
|
+
if (codeLower.includes('allowance[') && (codeLower.includes('=') || codeLower.includes('-='))) return 'transfer';
|
|
220
|
+
if (codeLower.includes('mint') || codeLower.includes('burn')) return 'mintburn';
|
|
221
|
+
return 'state';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if statement is an external call
|
|
226
|
+
*/
|
|
227
|
+
isExternalCall(stmt, code) {
|
|
228
|
+
const codeLower = code.toLowerCase();
|
|
229
|
+
|
|
230
|
+
const callPatterns = [
|
|
231
|
+
/\.call\s*\(/i,
|
|
232
|
+
/\.delegatecall\s*\(/i,
|
|
233
|
+
/\.send\s*\(/i,
|
|
234
|
+
/\.transfer\s*\(/i,
|
|
235
|
+
/external\s+contract/i
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
return callPatterns.some(pattern => pattern.test(code));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Find TOCTOU patterns (check -> external call -> use)
|
|
243
|
+
*/
|
|
244
|
+
findTOCTOUPatterns(checks, uses, externalCalls, node) {
|
|
245
|
+
// For each check-use pair, see if there's an external call in between
|
|
246
|
+
for (const check of checks) {
|
|
247
|
+
for (const use of uses) {
|
|
248
|
+
// Use must come after check
|
|
249
|
+
if (use.index <= check.index) continue;
|
|
250
|
+
|
|
251
|
+
// Check if there's an external call between check and use
|
|
252
|
+
const hasExternalCall = externalCalls.some(call =>
|
|
253
|
+
call.index > check.index && call.index < use.index
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Check if check and use are related (same variable/operation)
|
|
257
|
+
if (this.areRelated(check, use)) {
|
|
258
|
+
this.checkUsePairs.push({
|
|
259
|
+
check: check,
|
|
260
|
+
use: use,
|
|
261
|
+
hasExternalCall: hasExternalCall,
|
|
262
|
+
function: this.currentFunction,
|
|
263
|
+
node: node
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// If there's an external call, this is a TOCTOU vulnerability
|
|
267
|
+
if (hasExternalCall) {
|
|
268
|
+
this.reportTOCTOU(check, use, node);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Check if check and use are related
|
|
277
|
+
*/
|
|
278
|
+
areRelated(check, use) {
|
|
279
|
+
const checkCode = check.code.toLowerCase();
|
|
280
|
+
const useCode = use.code.toLowerCase();
|
|
281
|
+
|
|
282
|
+
// Balance check -> transfer or balance state modification
|
|
283
|
+
if (check.type === 'balance' && (use.type === 'transfer' || useCode.includes('balances['))) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Allowance check -> transfer or allowance state modification
|
|
288
|
+
if (check.type === 'allowance' && (use.type === 'transfer' || useCode.includes('allowance['))) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// State check -> state modification
|
|
293
|
+
if (check.type === 'state' && use.type === 'state') {
|
|
294
|
+
// Check if they reference the same variable
|
|
295
|
+
const checkVar = this.extractVariable(check.code);
|
|
296
|
+
const useVar = this.extractVariable(use.code);
|
|
297
|
+
return checkVar && useVar && checkVar === useVar;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Extract variable name from code
|
|
305
|
+
*/
|
|
306
|
+
extractVariable(code) {
|
|
307
|
+
// Simple extraction - look for common patterns
|
|
308
|
+
const patterns = [
|
|
309
|
+
/(\w+)\s*\.balance/i,
|
|
310
|
+
/balanceOf\s*\(\s*(\w+)/i,
|
|
311
|
+
/allowance\s*\(\s*(\w+)/i,
|
|
312
|
+
/(\w+)\s*==/i,
|
|
313
|
+
/(\w+)\s*!=/i
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
for (const pattern of patterns) {
|
|
317
|
+
const match = code.match(pattern);
|
|
318
|
+
if (match && match[1]) {
|
|
319
|
+
return match[1];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Report TOCTOU vulnerability
|
|
328
|
+
*/
|
|
329
|
+
reportTOCTOU(check, use, node) {
|
|
330
|
+
const funcName = this.currentFunction;
|
|
331
|
+
const checkType = check.type;
|
|
332
|
+
const useType = use.type;
|
|
333
|
+
|
|
334
|
+
let title, description, recommendation;
|
|
335
|
+
|
|
336
|
+
if (checkType === 'balance' && useType === 'transfer') {
|
|
337
|
+
title = 'TOCTOU: Balance Check Before Transfer';
|
|
338
|
+
description = `Function '${funcName}' checks balance before transfer, but an external call occurs between the check and use. An attacker can manipulate the balance during the external call, causing the transfer to use stale balance information.`;
|
|
339
|
+
recommendation = 'Cache balance value before external calls. Use Checks-Effects-Interactions pattern: update state first, then make external calls.';
|
|
340
|
+
} else if (checkType === 'allowance' && useType === 'transfer') {
|
|
341
|
+
title = 'TOCTOU: Allowance Check Before Transfer';
|
|
342
|
+
description = `Function '${funcName}' checks allowance before transferFrom, but an external call occurs between the check and use. An attacker can reduce allowance during the external call, causing the transfer to fail or use incorrect allowance.`;
|
|
343
|
+
recommendation = 'Cache allowance value before external calls. Consider using increaseAllowance/decreaseAllowance pattern instead of direct allowance checks.';
|
|
344
|
+
} else {
|
|
345
|
+
title = 'TOCTOU: State Check Before Use';
|
|
346
|
+
description = `Function '${funcName}' checks state before using it, but an external call occurs between the check and use. The state may change during the external call, leading to inconsistent behavior.`;
|
|
347
|
+
recommendation = 'Cache state values before external calls. Update state before making external calls when possible.';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.addFinding({
|
|
351
|
+
title: title,
|
|
352
|
+
description: description,
|
|
353
|
+
location: `Contract: ${this.currentContract}, Function: ${funcName}`,
|
|
354
|
+
line: check.statement.loc ? check.statement.loc.start.line : 0,
|
|
355
|
+
column: check.statement.loc ? check.statement.loc.start.column : 0,
|
|
356
|
+
code: `${check.code}\n...\n${use.code}`,
|
|
357
|
+
severity: 'HIGH',
|
|
358
|
+
confidence: 'HIGH',
|
|
359
|
+
exploitable: true,
|
|
360
|
+
exploitabilityScore: 80,
|
|
361
|
+
attackVector: 'toctou',
|
|
362
|
+
recommendation: recommendation,
|
|
363
|
+
references: [
|
|
364
|
+
'https://swcregistry.io/docs/SWC-107',
|
|
365
|
+
'https://consensys.github.io/smart-contract-best-practices/attacks/reentrancy/',
|
|
366
|
+
'https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html'
|
|
367
|
+
],
|
|
368
|
+
foundryPoC: this.generateTOCTOUPoC(this.currentContract, funcName, checkType, useType)
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Post-traversal analysis
|
|
374
|
+
*/
|
|
375
|
+
analyzeTOCTOUPatterns() {
|
|
376
|
+
// Additional analysis can be done here
|
|
377
|
+
// For example, finding patterns across multiple functions
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Generate Foundry PoC for TOCTOU
|
|
382
|
+
*/
|
|
383
|
+
generateTOCTOUPoC(contractName, funcName, checkType, useType) {
|
|
384
|
+
return `// SPDX-License-Identifier: MIT
|
|
385
|
+
pragma solidity ^0.8.0;
|
|
386
|
+
|
|
387
|
+
import "forge-std/Test.sol";
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Proof of Concept: TOCTOU Attack
|
|
391
|
+
* Target: ${contractName}.${funcName}()
|
|
392
|
+
* Attack Vector: State changes between check and use
|
|
393
|
+
*/
|
|
394
|
+
contract TOCTOUExploit is Test {
|
|
395
|
+
address constant TARGET = address(0); // ${contractName} address
|
|
396
|
+
AttackerContract attacker;
|
|
397
|
+
|
|
398
|
+
function setUp() public {
|
|
399
|
+
attacker = new AttackerContract();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function testExploit() public {
|
|
403
|
+
// 1. Setup: Attacker has some balance/allowance
|
|
404
|
+
// 2. Call vulnerable function which:
|
|
405
|
+
// - Checks balance/allowance (${checkType})
|
|
406
|
+
// - Makes external call (attacker's callback)
|
|
407
|
+
// - Uses cached/stale value for ${useType}
|
|
408
|
+
|
|
409
|
+
// 3. In callback, attacker manipulates state
|
|
410
|
+
// 4. Function continues with stale check value
|
|
411
|
+
|
|
412
|
+
// Assert exploit succeeded
|
|
413
|
+
// assertGt(attacker.balance, initialBalance);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
contract AttackerContract {
|
|
418
|
+
function onCallback() external {
|
|
419
|
+
// Manipulate state that was checked earlier
|
|
420
|
+
// This changes the state between check and use
|
|
421
|
+
}
|
|
422
|
+
}`;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
module.exports = TOCTOUDetector;
|
|
427
|
+
|