chainlink-audit 0.3.0 → 0.3.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/dist/rules/ccip.js +166 -7
- package/dist/scanner.js +1 -1
- package/package.json +1 -1
package/dist/rules/ccip.js
CHANGED
|
@@ -10,10 +10,23 @@ function hasCcipReceiver(content) {
|
|
|
10
10
|
return /(function\s+_ccipReceive\b|function\s+ccipReceive\b|is\s+CCIPReceiver)/.test(content);
|
|
11
11
|
}
|
|
12
12
|
function isCcipBaseReceiver(content) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
if (/interface\s+\w*I?Any2EVMMessageReceiver\b/.test(content))
|
|
14
|
+
return true;
|
|
15
|
+
if (/interface\s+\w+\s*{[\s\S]*function\s+ccipReceive\b[^{]*;/.test(content))
|
|
16
|
+
return true;
|
|
17
|
+
if (/abstract\s+contract\s+\w*CCIPReceiver\b/.test(content))
|
|
18
|
+
return true;
|
|
19
|
+
if (/function\s+_ccipReceive\b[^{;]*internal[^{;]*virtual\s*;/.test(content))
|
|
20
|
+
return true;
|
|
21
|
+
// Guard stub: ccipReceive body is only a revert — intentional "not a receiver" pattern (e.g. EVM2EVMOffRamp)
|
|
22
|
+
const ccipReceiveBody = extractFunctionBody(content, "ccipReceive");
|
|
23
|
+
if (ccipReceiveBody) {
|
|
24
|
+
const bodyTrimmed = ccipReceiveBody.trim();
|
|
25
|
+
if (/^\s*revert\s*\(\s*\)\s*;\s*$/.test(bodyTrimmed) ||
|
|
26
|
+
/^\s*revert\s+\w[\w.]*\s*\([^)]*\)\s*;\s*$/.test(bodyTrimmed))
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
17
30
|
}
|
|
18
31
|
function inheritsCcipReceiver(content) {
|
|
19
32
|
return /\bis\s+[\s\S{]*\bCCIPReceiver(Upgradeable)?\b/.test(content);
|
|
@@ -21,6 +34,20 @@ function inheritsCcipReceiver(content) {
|
|
|
21
34
|
function escapeRegExp(value) {
|
|
22
35
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
36
|
}
|
|
37
|
+
function extractParentNames(content) {
|
|
38
|
+
const match = /\bcontract\s+\w+\s+is\s+([\w\s,]+?)(?:\{|$)/m.exec(content);
|
|
39
|
+
if (!match)
|
|
40
|
+
return [];
|
|
41
|
+
return match[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
function inheritsViaCcipReceiver(content, repoSignals) {
|
|
44
|
+
const parents = extractParentNames(content);
|
|
45
|
+
return parents.some((parent) => {
|
|
46
|
+
const parentPattern = new RegExp(`\\b(?:abstract\\s+)?contract\\s+${escapeRegExp(parent)}\\b`);
|
|
47
|
+
const parentFile = repoSignals.files.find((f) => parentPattern.test(f.content));
|
|
48
|
+
return parentFile ? inheritsCcipReceiver(parentFile.content) : false;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
24
51
|
function delegatedCcipContent(body, repoSignals) {
|
|
25
52
|
const match = /\b([A-Z][A-Za-z0-9_]*)\s*\.\s*([A-Za-z0-9_]*ccipReceive|processCcipReceive)\s*\(/i.exec(body);
|
|
26
53
|
if (!match)
|
|
@@ -31,16 +58,28 @@ function delegatedCcipContent(body, repoSignals) {
|
|
|
31
58
|
const delegate = repoSignals.files.find((file) => typePattern.test(file.content) && functionPattern.test(file.content));
|
|
32
59
|
return delegate ? `${body}\n${delegate.content}` : body;
|
|
33
60
|
}
|
|
61
|
+
function stripEmitLines(body) {
|
|
62
|
+
return body
|
|
63
|
+
.split("\n")
|
|
64
|
+
.filter((line) => !/\bemit\b/.test(line))
|
|
65
|
+
.join("\n");
|
|
66
|
+
}
|
|
34
67
|
function validatesSourceChain(content, body) {
|
|
35
68
|
const header = ccipHeader(content);
|
|
36
|
-
|
|
69
|
+
// Strip event emissions before checking: sourceChainSelector that only appears in emit
|
|
70
|
+
// calls (e.g. for logging) must not suppress the finding.
|
|
71
|
+
const bodyWithoutEmits = stripEmitLines(body);
|
|
72
|
+
const directValidation = /(sourceChainSelector|ccipSelectorToChainId)/.test(bodyWithoutEmits) &&
|
|
37
73
|
/(require|revert|allowed|trusted|allowlist|supportedNetworks|UnsupportedNetwork)/i.test(body);
|
|
38
74
|
const modifierValidation = /(validChain|allowlisted|allowlist|trusted|onlyAllowlisted)[^{;]*(sourceChainSelector|\.sourceChainSelector)/i.test(header);
|
|
39
75
|
return directValidation || modifierValidation;
|
|
40
76
|
}
|
|
41
77
|
function validatesSourceSender(content, body) {
|
|
42
78
|
const header = ccipHeader(content);
|
|
43
|
-
|
|
79
|
+
// Strip event emissions before checking: sender that only appears in emit
|
|
80
|
+
// calls must not suppress the finding.
|
|
81
|
+
const bodyWithoutEmits = stripEmitLines(body);
|
|
82
|
+
const directValidation = /(message\.sender|data\.sender|sender)/.test(bodyWithoutEmits) &&
|
|
44
83
|
/(require|revert|allowed|trusted|allowlist|Unauthorized)/i.test(body);
|
|
45
84
|
const modifierValidation = /(validSender|trustedSender|allowlistedSender|onlyAllowlisted|allowlisted|allowlist|trusted)[^{;]*(message\.sender|\.sender|sender)/i.test(header);
|
|
46
85
|
return directValidation || modifierValidation;
|
|
@@ -64,6 +103,24 @@ function hasMessageIdTracking(content, body) {
|
|
|
64
103
|
return false;
|
|
65
104
|
return /(processed|received|executed|consumed|handled|seen|replay|duplicate|messageDetail|failedMessages|messageIdTo)/i.test(content);
|
|
66
105
|
}
|
|
106
|
+
function hasTokenPoolSender(content) {
|
|
107
|
+
return (/function\s+lockOrBurn\b/.test(content) &&
|
|
108
|
+
/(Pool\.LockOrBurnInV1|LockOrBurnInV1|is\s+[\w\s,]*TokenPool)/.test(content));
|
|
109
|
+
}
|
|
110
|
+
function hasTokenPoolReceiver(content) {
|
|
111
|
+
return (/function\s+releaseOrMint\b/.test(content) &&
|
|
112
|
+
/(Pool\.ReleaseOrMintInV1|ReleaseOrMintInV1|is\s+[\w\s,]*TokenPool)/.test(content));
|
|
113
|
+
}
|
|
114
|
+
function isTokenPoolBase(content) {
|
|
115
|
+
if (/abstract\s+contract\s+\w*TokenPool\b/.test(content))
|
|
116
|
+
return true;
|
|
117
|
+
if (/interface\s+\w*(TokenPool|IPool)\b/.test(content))
|
|
118
|
+
return true;
|
|
119
|
+
// Abstract contracts that inherit from a TokenPool base (e.g. BurnMintTokenPoolAbstract is BurnMintTokenPool)
|
|
120
|
+
if (/abstract\s+contract\s+\w+\s+is\s+[\w\s,]*TokenPool\b/.test(content))
|
|
121
|
+
return true;
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
67
124
|
export const ccipRules = [
|
|
68
125
|
{
|
|
69
126
|
metadata: {
|
|
@@ -145,7 +202,7 @@ export const ccipRules = [
|
|
|
145
202
|
const body = ccipBody(context.content) || context.content;
|
|
146
203
|
const validationContent = `${ccipHeader(context.content)}\n${delegatedCcipContent(body, context.repoSignals)}`;
|
|
147
204
|
const validatesRouter = /msg\.sender\s*(!=|==)\s*(router|s_router|i_router|address\(router\))|InvalidRouter|onlyRouter|NotCcipRouter|_msgSender\(\)\s*!=\s*address\([^)]*ccipRouter/i.test(validationContent);
|
|
148
|
-
if (validatesRouter || inheritsCcipReceiver(context.content))
|
|
205
|
+
if (validatesRouter || inheritsCcipReceiver(context.content) || inheritsViaCcipReceiver(context.content, context.repoSignals))
|
|
149
206
|
return [];
|
|
150
207
|
return [
|
|
151
208
|
makeFinding({
|
|
@@ -303,4 +360,106 @@ export const ccipRules = [
|
|
|
303
360
|
];
|
|
304
361
|
},
|
|
305
362
|
},
|
|
363
|
+
{
|
|
364
|
+
metadata: {
|
|
365
|
+
ruleId: "CL-CCIP-008",
|
|
366
|
+
product: "ccip",
|
|
367
|
+
severity: "high",
|
|
368
|
+
title: "Potential CCIP Token Pool lockOrBurn without _validateLockOrBurn",
|
|
369
|
+
description: "lockOrBurn override does not appear to call _validateLockOrBurn.",
|
|
370
|
+
},
|
|
371
|
+
scan(context) {
|
|
372
|
+
if (!hasTokenPoolSender(context.content))
|
|
373
|
+
return [];
|
|
374
|
+
if (isTokenPoolBase(context.content))
|
|
375
|
+
return [];
|
|
376
|
+
const body = extractFunctionBody(context.content, "lockOrBurn");
|
|
377
|
+
if (!body)
|
|
378
|
+
return [];
|
|
379
|
+
if (/_validateLockOrBurn\s*\(/.test(body))
|
|
380
|
+
return [];
|
|
381
|
+
return [
|
|
382
|
+
makeFinding({
|
|
383
|
+
context,
|
|
384
|
+
ruleId: this.metadata.ruleId,
|
|
385
|
+
severity: this.metadata.severity,
|
|
386
|
+
confidence: "medium",
|
|
387
|
+
line: firstLineMatching(context.lines, /function\s+lockOrBurn\b/),
|
|
388
|
+
title: this.metadata.title,
|
|
389
|
+
description: "Potential issue: lockOrBurn may skip rate limit and chain allowlist enforcement.",
|
|
390
|
+
risk: "Omitting _validateLockOrBurn bypasses CCIP-enforced rate limits and supported-chain checks, enabling unrestricted token locking or burning.",
|
|
391
|
+
recommendation: "Call _validateLockOrBurn(lockOrBurnIn) at the start of every lockOrBurn override before executing custom logic.",
|
|
392
|
+
}),
|
|
393
|
+
];
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
metadata: {
|
|
398
|
+
ruleId: "CL-CCIP-009",
|
|
399
|
+
product: "ccip",
|
|
400
|
+
severity: "high",
|
|
401
|
+
title: "Potential CCIP Token Pool releaseOrMint without _validateReleaseOrMint",
|
|
402
|
+
description: "releaseOrMint override does not appear to call _validateReleaseOrMint.",
|
|
403
|
+
},
|
|
404
|
+
scan(context) {
|
|
405
|
+
if (!hasTokenPoolReceiver(context.content))
|
|
406
|
+
return [];
|
|
407
|
+
if (isTokenPoolBase(context.content))
|
|
408
|
+
return [];
|
|
409
|
+
const body = extractFunctionBody(context.content, "releaseOrMint");
|
|
410
|
+
if (!body)
|
|
411
|
+
return [];
|
|
412
|
+
if (/_validateReleaseOrMint\s*\(/.test(body))
|
|
413
|
+
return [];
|
|
414
|
+
return [
|
|
415
|
+
makeFinding({
|
|
416
|
+
context,
|
|
417
|
+
ruleId: this.metadata.ruleId,
|
|
418
|
+
severity: this.metadata.severity,
|
|
419
|
+
confidence: "medium",
|
|
420
|
+
line: firstLineMatching(context.lines, /function\s+releaseOrMint\b/),
|
|
421
|
+
title: this.metadata.title,
|
|
422
|
+
description: "Potential issue: releaseOrMint may skip offRamp caller verification and rate limit enforcement.",
|
|
423
|
+
risk: "Omitting _validateReleaseOrMint allows arbitrary callers to trigger token minting or release, bypassing CCIP trust boundaries.",
|
|
424
|
+
recommendation: "Call _validateReleaseOrMint(releaseOrMintIn) at the start of every releaseOrMint override before minting or releasing tokens.",
|
|
425
|
+
}),
|
|
426
|
+
];
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
metadata: {
|
|
431
|
+
ruleId: "CL-CCIP-010",
|
|
432
|
+
product: "ccip",
|
|
433
|
+
severity: "medium",
|
|
434
|
+
title: "Potential unsafe CCIP Token Pool sourcePoolData decoding",
|
|
435
|
+
description: "releaseOrMint decodes sourcePoolData without obvious defensive checks.",
|
|
436
|
+
},
|
|
437
|
+
scan(context) {
|
|
438
|
+
if (!hasTokenPoolReceiver(context.content))
|
|
439
|
+
return [];
|
|
440
|
+
if (isTokenPoolBase(context.content))
|
|
441
|
+
return [];
|
|
442
|
+
const body = extractFunctionBody(context.content, "releaseOrMint");
|
|
443
|
+
if (!body)
|
|
444
|
+
return [];
|
|
445
|
+
if (!/abi\.decode\s*\(\s*(?:releaseOrMintIn\.)?sourcePoolData/.test(body))
|
|
446
|
+
return [];
|
|
447
|
+
const defensiveChecks = /(sourcePoolData\.length|try\s+this|catch|InvalidSourcePoolData|InvalidPayload|schema|version)/i.test(body);
|
|
448
|
+
if (defensiveChecks)
|
|
449
|
+
return [];
|
|
450
|
+
return [
|
|
451
|
+
makeFinding({
|
|
452
|
+
context,
|
|
453
|
+
ruleId: this.metadata.ruleId,
|
|
454
|
+
severity: this.metadata.severity,
|
|
455
|
+
confidence: "low",
|
|
456
|
+
line: firstLineMatching(context.lines, /abi\.decode\s*\(\s*(?:releaseOrMintIn\.)?sourcePoolData/),
|
|
457
|
+
title: this.metadata.title,
|
|
458
|
+
description: "Potential issue: malformed or unexpected sourcePoolData may cause releaseOrMint to revert and block token delivery.",
|
|
459
|
+
risk: "If sourcePoolData schema changes across an upgrade or pool version, decoding failures can permanently brick in-flight transfers.",
|
|
460
|
+
recommendation: "Validate sourcePoolData length or schema before decoding, or isolate decode failures so they can be handled without reverting the entire transfer.",
|
|
461
|
+
}),
|
|
462
|
+
];
|
|
463
|
+
},
|
|
464
|
+
},
|
|
306
465
|
];
|
package/dist/scanner.js
CHANGED
|
@@ -78,7 +78,7 @@ function detectProducts(files) {
|
|
|
78
78
|
const allContent = files.map((file) => file.content).join("\n");
|
|
79
79
|
if (/(AggregatorV3Interface|latestRoundData|IChainlinkAggregator)/.test(allContent))
|
|
80
80
|
products.add("data-feeds");
|
|
81
|
-
if (/(CCIPReceiver|Any2EVMMessage|IRouterClient|_ccipReceive|ccipReceive|sourceChainSelector)/.test(allContent))
|
|
81
|
+
if (/(CCIPReceiver|Any2EVMMessage|IRouterClient|_ccipReceive|ccipReceive|sourceChainSelector|TokenPool|LockOrBurnInV1|ReleaseOrMintInV1|_validateLockOrBurn|_validateReleaseOrMint)/.test(allContent))
|
|
82
82
|
products.add("ccip");
|
|
83
83
|
if (/(VRFConsumerBase|VRFCoordinator|fulfillRandomWords|requestRandomWords)/.test(allContent))
|
|
84
84
|
products.add("vrf");
|