brainblast 0.7.0 → 0.7.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/{chunk-34VXOLJF.js → chunk-B2M3TZSA.js} +1 -1
- package/dist/{chunk-CRYFCQYM.js → chunk-IY52XKWL.js} +87 -1
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{mcp-ML2X44WE.js → mcp-AM7MTCSZ.js} +1 -1
- package/dist/rules/anchor-pda-find-program-address.yaml +28 -0
- package/dist/rules/anchor-signer-constraint-missing.yaml +27 -0
- package/dist/rules/anchor-unchecked-account-type.yaml +27 -0
- package/package.json +1 -1
|
@@ -913,6 +913,89 @@ function anchorIdlAccount(c, params) {
|
|
|
913
913
|
};
|
|
914
914
|
}
|
|
915
915
|
|
|
916
|
+
// src/checkers/anchorAccountMissingConstraint.ts
|
|
917
|
+
var DEFAULT_AUTHORITY_RE = /^(authority|admin|owner|payer|caller|signer|user|operator|manager|creator|deployer)$/i;
|
|
918
|
+
function anchorAccountMissingConstraint(c, p) {
|
|
919
|
+
const namePattern = p?.nameRegex ? new RegExp(p.nameRegex) : DEFAULT_AUTHORITY_RE;
|
|
920
|
+
const authorityFields = c.accountFields.filter((f) => namePattern.test(f.name));
|
|
921
|
+
if (authorityFields.length === 0) {
|
|
922
|
+
return {
|
|
923
|
+
result: "cant_tell",
|
|
924
|
+
detail: p?.absentDetail ?? `Handler '${c.fnName}' has no authority-named account fields matching the pattern; rule does not apply.`
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
const missing = authorityFields.filter(
|
|
928
|
+
(f) => f.typeName.includes("AccountInfo") && !f.attrText.includes("signer") && !f.typeName.includes("Signer<")
|
|
929
|
+
);
|
|
930
|
+
if (missing.length > 0) {
|
|
931
|
+
const names = missing.map((f) => `'${f.name}'`).join(", ");
|
|
932
|
+
return {
|
|
933
|
+
result: "fail",
|
|
934
|
+
detail: p?.failDetail ?? `Handler '${c.fnName}': authority-named field(s) ${names} use AccountInfo<'info> without a \`signer\` constraint. Anchor performs no signing check on AccountInfo \u2014 any key can be passed as the authority and privileged instructions will execute without signature validation. Fix: change the type to Signer<'info>, or add #[account(signer)] to the field's constraint.`
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
result: "pass",
|
|
939
|
+
detail: p?.passDetail ?? `Handler '${c.fnName}': all authority-named fields use Signer<'info> or have a signer constraint.`
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/checkers/anchorForbiddenAccountType.ts
|
|
944
|
+
var DEFAULT_FORBIDDEN = "UncheckedAccount";
|
|
945
|
+
function anchorForbiddenAccountType(c, p) {
|
|
946
|
+
const forbidden = p?.forbiddenType ?? DEFAULT_FORBIDDEN;
|
|
947
|
+
const flagged = c.accountFields.filter((f) => f.typeName.includes(forbidden));
|
|
948
|
+
if (flagged.length === 0) {
|
|
949
|
+
if (c.accountFields.length > 0) {
|
|
950
|
+
return {
|
|
951
|
+
result: "pass",
|
|
952
|
+
detail: p?.passDetail ?? `Handler '${c.fnName}' has no '${forbidden}' account fields.`
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
result: "cant_tell",
|
|
957
|
+
detail: p?.absentDetail ?? `Handler '${c.fnName}' has no account fields to inspect; rule does not apply.`
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const names = flagged.map((f) => `'${f.name}'`).join(", ");
|
|
961
|
+
return {
|
|
962
|
+
result: "fail",
|
|
963
|
+
detail: p?.failDetail ?? `Handler '${c.fnName}' uses ${forbidden}<'info> on account(s) ${names}. ${forbidden} performs no runtime validation \u2014 ownership, signer status, and data layout are entirely unchecked. Replace with a typed account: Account<'info, T> (program-owned data), Signer<'info> (must sign), SystemAccount<'info> (system-owned), or InterfaceAccount<'info, T> (Token-2022 compatible).`
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/checkers/anchorBodyCallPattern.ts
|
|
968
|
+
function anchorBodyCallPattern(c, p) {
|
|
969
|
+
if (!p?.forbiddenPattern) {
|
|
970
|
+
return { result: "cant_tell", detail: "anchorBodyCallPattern: no forbiddenPattern param provided." };
|
|
971
|
+
}
|
|
972
|
+
const forbidden = new RegExp(p.forbiddenPattern);
|
|
973
|
+
const exempt = p?.exemptPattern ? new RegExp(p.exemptPattern) : null;
|
|
974
|
+
const hasForbidden = forbidden.test(c.fnBodyText);
|
|
975
|
+
if (!hasForbidden) {
|
|
976
|
+
if (c.fnBodyText.trim().length > 0) {
|
|
977
|
+
return {
|
|
978
|
+
result: "pass",
|
|
979
|
+
detail: p?.passDetail ?? `Handler '${c.fnName}' does not contain the forbidden pattern '${p.forbiddenPattern}'.`
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
result: "cant_tell",
|
|
984
|
+
detail: p?.absentDetail ?? `Handler '${c.fnName}' has an empty body; rule does not apply.`
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
if (exempt && exempt.test(c.fnBodyText)) {
|
|
988
|
+
return {
|
|
989
|
+
result: "pass",
|
|
990
|
+
detail: p?.passDetail ?? `Handler '${c.fnName}' contains '${p.forbiddenPattern}' but also matches the exemption pattern \u2014 considered safe.`
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
return {
|
|
994
|
+
result: "fail",
|
|
995
|
+
detail: p?.failDetail ?? `Handler '${c.fnName}' body contains '${p.forbiddenPattern}', which is a known footgun pattern. Use the Anchor seeds + bump constraint on the Accounts struct instead of deriving PDAs at runtime.`
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
916
999
|
// src/checkers/index.ts
|
|
917
1000
|
var registry = {
|
|
918
1001
|
"positional-arg-identity": positionalArgIdentity,
|
|
@@ -927,7 +1010,10 @@ var registry = {
|
|
|
927
1010
|
"literal-multiplier-wrong-constant": literalMultiplierWrongConstant,
|
|
928
1011
|
"forbidden-call-replacement": forbiddenCallReplacement,
|
|
929
1012
|
"solana-mint-identity-mismatch": solanaMintIdentity,
|
|
930
|
-
"anchor-account-matches-idl": anchorIdlAccount
|
|
1013
|
+
"anchor-account-matches-idl": anchorIdlAccount,
|
|
1014
|
+
"anchor-account-missing-constraint": anchorAccountMissingConstraint,
|
|
1015
|
+
"anchor-forbidden-account-type": anchorForbiddenAccountType,
|
|
1016
|
+
"anchor-body-call-pattern": anchorBodyCallPattern
|
|
931
1017
|
};
|
|
932
1018
|
function runChecker(kind, c, params) {
|
|
933
1019
|
const fn = registry[kind];
|
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
submitTelemetry,
|
|
12
12
|
telemetryFilePath,
|
|
13
13
|
validatePack
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-B2M3TZSA.js";
|
|
15
15
|
import {
|
|
16
16
|
renderTrustGraphMd
|
|
17
17
|
} from "./chunk-2UZGWXIX.js";
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
audit,
|
|
26
26
|
getChangedRanges,
|
|
27
27
|
resolveRules
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-IY52XKWL.js";
|
|
29
29
|
import "./chunk-2XJORJPQ.js";
|
|
30
30
|
import "./chunk-O5Z4ZJHC.js";
|
|
31
31
|
import "./chunk-XSVQSK53.js";
|
|
@@ -128,7 +128,7 @@ if (args[0] === "drift") {
|
|
|
128
128
|
process.exit(0);
|
|
129
129
|
}
|
|
130
130
|
if (args[0] === "mcp") {
|
|
131
|
-
const { startMcpServer } = await import("./mcp-
|
|
131
|
+
const { startMcpServer } = await import("./mcp-AM7MTCSZ.js");
|
|
132
132
|
await startMcpServer();
|
|
133
133
|
process.exit(0);
|
|
134
134
|
}
|
package/dist/index.js
CHANGED
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
submitTelemetry,
|
|
55
55
|
telemetryFilePath,
|
|
56
56
|
validatePack
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-B2M3TZSA.js";
|
|
58
58
|
import {
|
|
59
59
|
renderTrustGraphMd
|
|
60
60
|
} from "./chunk-2UZGWXIX.js";
|
|
@@ -91,7 +91,7 @@ import {
|
|
|
91
91
|
runChecker,
|
|
92
92
|
testKinds,
|
|
93
93
|
validatePackManifest
|
|
94
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-IY52XKWL.js";
|
|
95
95
|
import {
|
|
96
96
|
CANONICAL_BY_MINT,
|
|
97
97
|
CANONICAL_MINTS,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
id: anchor-pda-find-program-address
|
|
2
|
+
severity: high
|
|
3
|
+
title: Anchor handler calls find_program_address at runtime instead of using seeds+bump constraint
|
|
4
|
+
component:
|
|
5
|
+
name: Anchor Framework
|
|
6
|
+
type: Blockchain
|
|
7
|
+
version: ">=0.26.0"
|
|
8
|
+
sourceUrl: https://www.anchor-lang.com/docs/the-accounts-struct#seeds-and-bump
|
|
9
|
+
detect:
|
|
10
|
+
lang: rust
|
|
11
|
+
modules:
|
|
12
|
+
- "@coral-xyz/anchor"
|
|
13
|
+
- "@project-serum/anchor"
|
|
14
|
+
nameRegex: ".*"
|
|
15
|
+
triggerCalls: []
|
|
16
|
+
check:
|
|
17
|
+
kind: anchor-body-call-pattern
|
|
18
|
+
params:
|
|
19
|
+
forbiddenPattern: "Pubkey::find_program_address|find_program_address\\s*\\("
|
|
20
|
+
test:
|
|
21
|
+
kind: anchor-program-test
|
|
22
|
+
params:
|
|
23
|
+
scenario: pda-canonical-bump-mismatch
|
|
24
|
+
description: >-
|
|
25
|
+
Initializes the PDA storing the canonical bump, then calls the handler with
|
|
26
|
+
a non-canonical bump seed. With find_program_address the handler may accept
|
|
27
|
+
this (re-deriving a different nonce), whereas the seeds+bump constraint
|
|
28
|
+
enforces the stored canonical bump and will reject it.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
id: anchor-signer-constraint-missing
|
|
2
|
+
severity: critical
|
|
3
|
+
title: Anchor authority account missing signer constraint
|
|
4
|
+
component:
|
|
5
|
+
name: Anchor Framework
|
|
6
|
+
type: Blockchain
|
|
7
|
+
version: ">=0.26.0"
|
|
8
|
+
sourceUrl: https://www.anchor-lang.com/docs/the-accounts-struct#signer
|
|
9
|
+
detect:
|
|
10
|
+
lang: rust
|
|
11
|
+
modules:
|
|
12
|
+
- "@coral-xyz/anchor"
|
|
13
|
+
- "@project-serum/anchor"
|
|
14
|
+
nameRegex: ".*"
|
|
15
|
+
triggerCalls: []
|
|
16
|
+
check:
|
|
17
|
+
kind: anchor-account-missing-constraint
|
|
18
|
+
params:
|
|
19
|
+
nameRegex: "^(authority|admin|owner|payer|caller|signer|user|operator|manager|creator|deployer)$"
|
|
20
|
+
test:
|
|
21
|
+
kind: anchor-program-test
|
|
22
|
+
params:
|
|
23
|
+
scenario: unauthorized-caller
|
|
24
|
+
description: >-
|
|
25
|
+
Calls the instruction with a key that does not match the expected authority
|
|
26
|
+
and has not signed the transaction. Must be rejected with a signing error.
|
|
27
|
+
If it succeeds, the authority check is absent and privilege escalation is possible.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
id: anchor-unchecked-account-type
|
|
2
|
+
severity: high
|
|
3
|
+
title: Anchor instruction uses UncheckedAccount — no runtime validation
|
|
4
|
+
component:
|
|
5
|
+
name: Anchor Framework
|
|
6
|
+
type: Blockchain
|
|
7
|
+
version: ">=0.26.0"
|
|
8
|
+
sourceUrl: https://www.anchor-lang.com/docs/the-accounts-struct#uncheckedaccount
|
|
9
|
+
detect:
|
|
10
|
+
lang: rust
|
|
11
|
+
modules:
|
|
12
|
+
- "@coral-xyz/anchor"
|
|
13
|
+
- "@project-serum/anchor"
|
|
14
|
+
nameRegex: ".*"
|
|
15
|
+
triggerCalls: []
|
|
16
|
+
check:
|
|
17
|
+
kind: anchor-forbidden-account-type
|
|
18
|
+
params:
|
|
19
|
+
forbiddenType: "UncheckedAccount"
|
|
20
|
+
test:
|
|
21
|
+
kind: anchor-program-test
|
|
22
|
+
params:
|
|
23
|
+
scenario: arbitrary-account-substitution
|
|
24
|
+
description: >-
|
|
25
|
+
Passes an account owned by an arbitrary program (not the expected program ID)
|
|
26
|
+
as the UncheckedAccount field. Must be rejected. If it succeeds, an attacker
|
|
27
|
+
can substitute any account and the instruction will operate on attacker-controlled data.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainblast",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic auditor for catastrophic AI-integration bugs: scan a repo, find the silent money/auth traps, and generate the behavioral test that proves they're fixed.",
|
|
6
6
|
"keywords": [
|