anchor-audit 0.1.0
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 +28 -0
- package/dist/auditor.d.ts +16 -0
- package/dist/auditor.js +235 -0
- package/dist/auditor.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +96 -0
- package/dist/index.js.map +1 -0
- package/dist/metadata.d.ts +25 -0
- package/dist/metadata.js +48 -0
- package/dist/metadata.js.map +1 -0
- package/dist/reporter.d.ts +18 -0
- package/dist/reporter.js +177 -0
- package/dist/reporter.js.map +1 -0
- package/dist/rules-loader.d.ts +12 -0
- package/dist/rules-loader.js +65 -0
- package/dist/rules-loader.js.map +1 -0
- package/dist/scanner.d.ts +6 -0
- package/dist/scanner.js +42 -0
- package/dist/scanner.js.map +1 -0
- package/package.json +41 -0
- package/rules/001-missing-signer-check.md +57 -0
- package/rules/002-missing-owner-check.md +53 -0
- package/rules/003-missing-discriminator-check.md +53 -0
- package/rules/004-account-substitution.md +54 -0
- package/rules/005-sysvar-spoofing.md +57 -0
- package/rules/006-missing-rent-exemption-check.md +47 -0
- package/rules/007-account-aliasing.md +53 -0
- package/rules/008-uninitialized-account-use.md +53 -0
- package/rules/009-missing-mut-constraint.md +52 -0
- package/rules/010-missing-close-constraint.md +48 -0
- package/rules/011-pda-seed-collision.md +49 -0
- package/rules/012-missing-bump-validation.md +55 -0
- package/rules/013-non-canonical-bump-accepted.md +52 -0
- package/rules/014-predictable-pda.md +57 -0
- package/rules/015-insecure-pda-across-upgrades.md +54 -0
- package/rules/016-bump-mismatch.md +49 -0
- package/rules/017-arbitrary-cpi.md +56 -0
- package/rules/018-cpi-confused-deputy.md +50 -0
- package/rules/019-missing-program-id-check-spl.md +51 -0
- package/rules/020-reentrancy-via-cpi.md +50 -0
- package/rules/021-untrusted-callback.md +49 -0
- package/rules/022-cpi-with-attacker-accounts.md +58 -0
- package/rules/023-lamport-overflow.md +50 -0
- package/rules/024-token-amount-overflow.md +52 -0
- package/rules/025-precision-loss.md +42 -0
- package/rules/026-rounding-direction.md +43 -0
- package/rules/027-token-decimal-mismatch.md +50 -0
- package/rules/028-integer-cast-truncation.md +42 -0
- package/rules/029-off-by-one.md +45 -0
- package/rules/030-missing-authorization.md +51 -0
- package/rules/031-reinitialization-attack.md +50 -0
- package/rules/032-closed-account-revival.md +49 -0
- package/rules/033-init-if-needed-misuse.md +66 -0
- package/rules/034-missing-has-one.md +55 -0
- package/rules/035-insecure-admin-transfer.md +49 -0
- package/rules/036-missing-pause-guards.md +50 -0
- package/rules/037-clock-manipulation.md +53 -0
- package/rules/038-missing-address-validation.md +53 -0
- package/rules/039-constraint-evaluation-stage.md +54 -0
- package/rules/040-realloc-zero-init.md +53 -0
- package/rules/041-missing-payer-on-init.md +59 -0
- package/rules/042-incorrect-space-allocation.md +53 -0
- package/rules/043-account-vs-account-info.md +53 -0
- package/rules/044-token-account-owner-unverified.md +56 -0
- package/rules/045-token-mint-unverified.md +58 -0
- package/rules/046-ata-assumption-errors.md +53 -0
- package/rules/047-token-program-id-hardcoded.md +55 -0
- package/rules/048-compute-budget-abuse.md +51 -0
- package/rules/049-log-spam-dos.md +49 -0
- package/rules/050-stack-overflow-deep-cpi.md +48 -0
- package/rules/INDEX.md +65 -0
- package/rules/README.md +40 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Rule 043: `Account` vs `AccountInfo` Misuse
|
|
2
|
+
|
|
3
|
+
**Severity:** High
|
|
4
|
+
**Category:** Constraints
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
Choosing `AccountInfo<'info>` / `UncheckedAccount<'info>` where `Account<'info, T>` (or a typed wrapper like `Program`, `Signer`, `Sysvar`, `InterfaceAccount`) belongs throws away Anchor's automatic validation: owner check, discriminator check, and deserialization. Reaching for the untyped variant "to make it compile" silently disables the protections that prevent owner-spoofing (Rule 002) and type-cosplay (Rule 003).
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
#[derive(Accounts)]
|
|
12
|
+
pub struct Use<'info> {
|
|
13
|
+
/// CHECK: it's our config
|
|
14
|
+
pub config: AccountInfo<'info>, // no owner/discriminator/type check
|
|
15
|
+
pub user: Signer<'info>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn use_config(ctx: Context<Use>) -> Result<()> {
|
|
19
|
+
let config = Config::try_from_slice(&ctx.accounts.config.data.borrow())?; // unsafe
|
|
20
|
+
require_keys_eq!(config.admin, ctx.accounts.user.key());
|
|
21
|
+
Ok(())
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Why this is dangerous
|
|
26
|
+
With `AccountInfo`, nothing verifies that `config` is owned by this program or is actually a `Config` — the attacker forges its bytes (Rule 002) or passes a different account type with a compatible layout (Rule 003). The single type choice is the difference between Anchor enforcing three invariants and the program enforcing none.
|
|
27
|
+
|
|
28
|
+
## Fix pattern
|
|
29
|
+
```rust
|
|
30
|
+
#[derive(Accounts)]
|
|
31
|
+
pub struct Use<'info> {
|
|
32
|
+
// Account<'info, Config> checks owner == program ID, discriminator, and
|
|
33
|
+
// deserializes safely.
|
|
34
|
+
#[account(has_one = admin)]
|
|
35
|
+
pub config: Account<'info, Config>,
|
|
36
|
+
pub admin: Signer<'info>,
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
Reserve `AccountInfo`/`UncheckedAccount` for genuinely opaque accounts, and when used, document the manual checks in the `/// CHECK:` comment and perform them.
|
|
40
|
+
|
|
41
|
+
## Detection heuristic
|
|
42
|
+
- `AccountInfo`/`UncheckedAccount` whose data is deserialized into a known program type
|
|
43
|
+
- `/// CHECK:` comments that don't describe a real, performed validation
|
|
44
|
+
- Program/token/sysvar accounts typed as `AccountInfo` instead of `Program`/`Account<TokenAccount>`/`Sysvar`
|
|
45
|
+
- Typed wrappers avoided "to fix a lifetime/borrow error" without restoring the checks manually
|
|
46
|
+
|
|
47
|
+
## References
|
|
48
|
+
- Anchor docs — account types (https://www.anchor-lang.com/docs/account-types)
|
|
49
|
+
- The Anchor Book — AccountInfo and UncheckedAccount (https://book.anchor-lang.com/anchor_in_depth/the_accounts_struct.html)
|
|
50
|
+
- Neodyme — Solana common pitfalls (https://neodyme.io/en/blog/solana_common_pitfalls/)
|
|
51
|
+
|
|
52
|
+
## Real-world exploits (if any)
|
|
53
|
+
No single attributed exploit for the type choice alone; it is the enabling mistake behind owner-check and type-cosplay exploits (Rules 002, 003) repeatedly flagged in audits.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Rule 044: Token Account Owner Unverified
|
|
2
|
+
|
|
3
|
+
**Severity:** High
|
|
4
|
+
**Category:** SPL Token
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
An SPL token account has an `owner` field (the wallet/PDA authority that controls it) distinct from the account's program owner. Code that accepts a token account and acts on it — crediting a user, treating it as a vault — without verifying its `owner` matches the expected authority lets an attacker pass a token account controlled by someone else, or by the program when it shouldn't be.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
#[derive(Accounts)]
|
|
12
|
+
pub struct Stake<'info> {
|
|
13
|
+
pub user: Signer<'info>,
|
|
14
|
+
// No check that user_token.owner == user.key()
|
|
15
|
+
#[account(mut)]
|
|
16
|
+
pub user_token: Account<'info, TokenAccount>,
|
|
17
|
+
#[account(mut)]
|
|
18
|
+
pub stake_account: Account<'info, StakeAccount>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub fn stake(ctx: Context<Stake>, amount: u64) -> Result<()> {
|
|
22
|
+
// Credits the signer based on a token account they may not own
|
|
23
|
+
ctx.accounts.stake_account.staked += amount;
|
|
24
|
+
Ok(())
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Why this is dangerous
|
|
29
|
+
The attacker references a token account they don't control (or whose balance they can't actually move) to claim staking credit, or substitutes the program's own vault as the "source" to get credited without depositing. The token account's `owner`/`authority` must be tied to the principal the instruction credits or debits.
|
|
30
|
+
|
|
31
|
+
## Fix pattern
|
|
32
|
+
```rust
|
|
33
|
+
#[derive(Accounts)]
|
|
34
|
+
pub struct Stake<'info> {
|
|
35
|
+
pub user: Signer<'info>,
|
|
36
|
+
#[account(mut, token::authority = user)] // owner must be `user`
|
|
37
|
+
pub user_token: Account<'info, TokenAccount>,
|
|
38
|
+
#[account(mut, has_one = user)]
|
|
39
|
+
pub stake_account: Account<'info, StakeAccount>,
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
Use `token::authority = ...` (or check `token_account.owner == expected`) on every token account whose control matters.
|
|
43
|
+
|
|
44
|
+
## Detection heuristic
|
|
45
|
+
- `Account<'info, TokenAccount>` used in logic without `token::authority` / an `owner` equality check
|
|
46
|
+
- Crediting/debiting a principal based on a token account not constrained to that principal
|
|
47
|
+
- Vault/source token accounts not pinned via `token::authority = <pda>` or `address =`
|
|
48
|
+
- `.owner` of a token account read but compared to nothing
|
|
49
|
+
|
|
50
|
+
## References
|
|
51
|
+
- anchor_spl docs — token::authority constraint (https://docs.rs/anchor-spl/latest/anchor_spl/)
|
|
52
|
+
- SPL Token docs — account owner vs authority (https://spl.solana.com/token)
|
|
53
|
+
- Neodyme — Solana common pitfalls (https://neodyme.io/en/blog/solana_common_pitfalls/)
|
|
54
|
+
|
|
55
|
+
## Real-world exploits (if any)
|
|
56
|
+
No single attributed public headline exploit; unverified token-account ownership is a frequent high-severity audit finding in staking and vault programs.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Rule 045: Token Mint Unverified
|
|
2
|
+
|
|
3
|
+
**Severity:** High
|
|
4
|
+
**Category:** SPL Token
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
A token account is tied to exactly one mint, but code that accepts "a token account" without constraining its `mint` allows an attacker to pass an account for a *different*, worthless mint. The program credits or values the deposit as if it were the expected (valuable) token, because it never checked which token the account actually holds.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
#[derive(Accounts)]
|
|
12
|
+
pub struct Deposit<'info> {
|
|
13
|
+
pub user: Signer<'info>,
|
|
14
|
+
// No check that user_token.mint == expected USDC mint
|
|
15
|
+
#[account(mut)]
|
|
16
|
+
pub user_token: Account<'info, TokenAccount>,
|
|
17
|
+
#[account(mut)]
|
|
18
|
+
pub vault: Account<'info, TokenAccount>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
|
|
22
|
+
// Transfers `amount` of WHATEVER mint user_token holds, credits as USDC
|
|
23
|
+
token::transfer(/* user_token -> vault */, amount)?;
|
|
24
|
+
ctx.accounts.position.usdc_balance += amount;
|
|
25
|
+
Ok(())
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why this is dangerous
|
|
30
|
+
The attacker mints a worthless token, deposits it, and is credited a USDC balance one-for-one, then withdraws real USDC. Without a mint check, the program cannot tell a valuable token from a fake one. The transfer must also target a vault of the *same* mint, or accounting diverges from the actual held assets.
|
|
31
|
+
|
|
32
|
+
## Fix pattern
|
|
33
|
+
```rust
|
|
34
|
+
#[derive(Accounts)]
|
|
35
|
+
pub struct Deposit<'info> {
|
|
36
|
+
pub user: Signer<'info>,
|
|
37
|
+
#[account(mut, token::mint = usdc_mint, token::authority = user)]
|
|
38
|
+
pub user_token: Account<'info, TokenAccount>,
|
|
39
|
+
#[account(mut, token::mint = usdc_mint)]
|
|
40
|
+
pub vault: Account<'info, TokenAccount>,
|
|
41
|
+
#[account(address = config.usdc_mint)]
|
|
42
|
+
pub usdc_mint: Account<'info, Mint>,
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Detection heuristic
|
|
47
|
+
- Token accounts used without a `token::mint = ...` constraint or `.mint` equality check
|
|
48
|
+
- Deposit/withdraw/swap logic that credits a specific asset from an unconstrained token account
|
|
49
|
+
- Vault and user token accounts not constrained to the *same* expected mint
|
|
50
|
+
- A `Mint` account referenced but never pinned via `address =` to the configured mint
|
|
51
|
+
|
|
52
|
+
## References
|
|
53
|
+
- anchor_spl docs — token::mint constraint (https://docs.rs/anchor-spl/latest/anchor_spl/)
|
|
54
|
+
- SPL Token docs — token account mint binding (https://spl.solana.com/token)
|
|
55
|
+
- Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
|
|
56
|
+
|
|
57
|
+
## Real-world exploits (if any)
|
|
58
|
+
No single attributed public headline exploit; missing mint validation is a recurring high/critical finding in audits of deposit-based DeFi programs.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Rule 046: Associated Token Account Assumption Errors
|
|
2
|
+
|
|
3
|
+
**Severity:** Medium
|
|
4
|
+
**Category:** SPL Token
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
Code assumes a token account is *the* canonical associated token account (ATA) for a given wallet+mint — correctly derived, already existing, and the only one — without enforcing it. A wallet can hold many token accounts for the same mint, and only the ATA is at the deterministic derived address. Trusting an unconstrained "ATA" lets an attacker pass a non-canonical token account, and assuming existence causes failures or, worse, silent misrouting.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
#[derive(Accounts)]
|
|
12
|
+
pub struct Payout<'info> {
|
|
13
|
+
/// CHECK: assumed to be the recipient's ATA, but not derived/verified
|
|
14
|
+
#[account(mut)]
|
|
15
|
+
pub recipient_ata: AccountInfo<'info>,
|
|
16
|
+
pub recipient: SystemAccount<'info>,
|
|
17
|
+
pub mint: Account<'info, Mint>,
|
|
18
|
+
// ...
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Why this is dangerous
|
|
23
|
+
The attacker passes a token account they control (for the right mint but at a non-ATA address, or owned by a different wallet) as `recipient_ata`, redirecting a payout. Conversely, assuming the ATA already exists makes the instruction fail when it doesn't, bricking the flow — or pushes developers to create it without checking who pays. ATA identity must be derived and checked, not assumed.
|
|
24
|
+
|
|
25
|
+
## Fix pattern
|
|
26
|
+
```rust
|
|
27
|
+
#[derive(Accounts)]
|
|
28
|
+
pub struct Payout<'info> {
|
|
29
|
+
#[account(
|
|
30
|
+
mut,
|
|
31
|
+
associated_token::mint = mint,
|
|
32
|
+
associated_token::authority = recipient, // enforces canonical ATA
|
|
33
|
+
)]
|
|
34
|
+
pub recipient_ata: Account<'info, TokenAccount>,
|
|
35
|
+
pub recipient: SystemAccount<'info>,
|
|
36
|
+
pub mint: Account<'info, Mint>,
|
|
37
|
+
// For creation: use `init` with associated_token::* + the ATA program.
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Detection heuristic
|
|
42
|
+
- Accounts named `*_ata` typed as `AccountInfo` or `TokenAccount` without `associated_token::*` constraints
|
|
43
|
+
- Token destinations assumed canonical without `associated_token::mint`/`authority` (or a `get_associated_token_address` comparison)
|
|
44
|
+
- Code that assumes an ATA exists with no `init`/`init_if_needed` or existence handling
|
|
45
|
+
- ATA derivation done off-chain and trusted on-chain without re-derivation
|
|
46
|
+
|
|
47
|
+
## References
|
|
48
|
+
- anchor_spl docs — associated_token constraints (https://docs.rs/anchor-spl/latest/anchor_spl/associated_token/)
|
|
49
|
+
- SPL Associated Token Account program docs (https://spl.solana.com/associated-token-account)
|
|
50
|
+
- Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
|
|
51
|
+
|
|
52
|
+
## Real-world exploits (if any)
|
|
53
|
+
No single attributed public exploit; ATA assumption errors are common medium audit findings, especially payout redirection via non-canonical token accounts.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Rule 047: Token Program ID Hardcoded vs. Validated
|
|
2
|
+
|
|
3
|
+
**Severity:** Medium
|
|
4
|
+
**Category:** SPL Token
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
With SPL Token and Token-2022 both in use, a program must be deliberate about which token program it targets. Two failure modes: (1) hardcoding `spl_token::ID` while accepting mints/accounts that actually belong to Token-2022 (or vice-versa), causing CPI failures or wrong assumptions; and (2) accepting the token program as an unvalidated account so the wrong (or fake) program is used. The program ID must match the program that actually owns the token accounts involved.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
pub fn transfer_out(ctx: Context<TransferOut>, amount: u64) -> Result<()> {
|
|
12
|
+
// Hardcodes legacy SPL Token, but the mint is a Token-2022 mint, so the
|
|
13
|
+
// accounts are owned by the Token-2022 program — this CPI targets the
|
|
14
|
+
// wrong program ID for these accounts.
|
|
15
|
+
let cpi = CpiContext::new(
|
|
16
|
+
ctx.accounts.token_program.to_account_info(), // assumed spl_token::ID
|
|
17
|
+
Transfer { /* ... */ },
|
|
18
|
+
);
|
|
19
|
+
token::transfer(cpi, amount)?;
|
|
20
|
+
Ok(())
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Why this is dangerous
|
|
25
|
+
Mismatching the token program against the token accounts' actual owner program makes transfers fail (availability) or, when combined with unvalidated program accounts, lets a fake token program be injected (Rule 019). Hardcoding one variant silently breaks compatibility for the other and can be steered by an attacker choosing a Token-2022 mint where SPL Token is assumed.
|
|
26
|
+
|
|
27
|
+
## Fix pattern
|
|
28
|
+
```rust
|
|
29
|
+
#[derive(Accounts)]
|
|
30
|
+
pub struct TransferOut<'info> {
|
|
31
|
+
// Interface accepts either SPL Token or Token-2022, verified to match
|
|
32
|
+
// the accounts' owning program.
|
|
33
|
+
pub token_program: Interface<'info, TokenInterface>,
|
|
34
|
+
#[account(mut)]
|
|
35
|
+
pub from: InterfaceAccount<'info, TokenAccount>,
|
|
36
|
+
#[account(mut)]
|
|
37
|
+
pub to: InterfaceAccount<'info, TokenAccount>,
|
|
38
|
+
pub authority: Signer<'info>,
|
|
39
|
+
}
|
|
40
|
+
// Use anchor_spl::token_interface CPIs so the call routes to the correct program.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Detection heuristic
|
|
44
|
+
- Hardcoded `spl_token::ID` / `Program<'info, Token>` while the program intends to support Token-2022 (or vice-versa)
|
|
45
|
+
- Token accounts as `Account<TokenAccount>` (legacy) when mints may be Token-2022 — use `InterfaceAccount`
|
|
46
|
+
- Token program passed as `AccountInfo` without an ID check (overlaps Rule 019)
|
|
47
|
+
- No verification that `token_program.key()` equals the owner program of the passed token accounts
|
|
48
|
+
|
|
49
|
+
## References
|
|
50
|
+
- anchor_spl docs — token_interface / Interface (https://docs.rs/anchor-spl/latest/anchor_spl/token_interface/)
|
|
51
|
+
- SPL Token-2022 docs (https://spl.solana.com/token-2022)
|
|
52
|
+
- Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
|
|
53
|
+
|
|
54
|
+
## Real-world exploits (if any)
|
|
55
|
+
No single attributed public exploit; token-program mismatch and Token-2022 compatibility gaps are increasingly common medium audit findings.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Rule 048: Compute Budget Abuse (Unbounded Work)
|
|
2
|
+
|
|
3
|
+
**Severity:** Medium
|
|
4
|
+
**Category:** Runtime
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
Each Solana transaction has a compute-unit budget. An instruction that iterates over an attacker-controllable, unbounded collection — `remaining_accounts`, a `Vec` field that can grow without limit, or a loop whose count comes from input — can be pushed to exceed the budget and fail. If a critical operation (liquidation, settlement, crank) lives behind such a loop, the attacker grows the data until the operation can no longer complete, a denial-of-service.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
pub fn distribute(ctx: Context<Distribute>) -> Result<()> {
|
|
12
|
+
// `holders` can grow unboundedly as users join; eventually this loop
|
|
13
|
+
// exceeds the compute budget and every distribute() call fails.
|
|
14
|
+
for holder in ctx.accounts.registry.holders.iter() {
|
|
15
|
+
pay(holder)?;
|
|
16
|
+
}
|
|
17
|
+
Ok(())
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Why this is dangerous
|
|
22
|
+
Once the collection is large enough, the instruction always runs out of compute and reverts, permanently bricking the path. If liquidations or withdrawals depend on it, funds can be frozen. An attacker may intentionally inflate the collection (cheap entries) to trigger the DoS, or simply rely on organic growth crossing the limit.
|
|
23
|
+
|
|
24
|
+
## Fix pattern
|
|
25
|
+
```rust
|
|
26
|
+
// Paginate / bound the work per call, tracking progress in state:
|
|
27
|
+
pub fn distribute(ctx: Context<Distribute>, start: u32, count: u32) -> Result<()> {
|
|
28
|
+
require!(count <= MAX_PER_CALL, ErrorCode::BatchTooLarge);
|
|
29
|
+
let end = (start + count).min(ctx.accounts.registry.holders.len() as u32);
|
|
30
|
+
for i in start..end {
|
|
31
|
+
pay(&ctx.accounts.registry.holders[i as usize])?;
|
|
32
|
+
}
|
|
33
|
+
ctx.accounts.registry.cursor = end;
|
|
34
|
+
Ok(())
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
Cap collection sizes at insertion and design cranks to process bounded batches.
|
|
38
|
+
|
|
39
|
+
## Detection heuristic
|
|
40
|
+
- Loops over `remaining_accounts`, `Vec`/`String` fields, or input-controlled counts with no per-call cap
|
|
41
|
+
- Account structs holding unbounded collections that are iterated in a single instruction
|
|
42
|
+
- Critical operations (liquidate, settle, distribute) gated behind whole-collection iteration
|
|
43
|
+
- No pagination/cursor for processing large datasets
|
|
44
|
+
|
|
45
|
+
## References
|
|
46
|
+
- Solana docs — compute budget and limits (https://solana.com/docs/core/fees#compute-budget)
|
|
47
|
+
- Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
|
|
48
|
+
- Sec3 — denial-of-service via compute exhaustion (https://www.sec3.dev/blog)
|
|
49
|
+
|
|
50
|
+
## Real-world exploits (if any)
|
|
51
|
+
No single attributed public exploit; unbounded-iteration DoS is a recognized medium audit finding for registries, reward distributors, and cranks.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Rule 049: Log Spam / Excessive Logging DoS
|
|
2
|
+
|
|
3
|
+
**Severity:** Low
|
|
4
|
+
**Category:** Runtime
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
`msg!` and other logging consume compute units and contribute to transaction log size, which is itself bounded. Logging attacker-controlled or unbounded data (a user-supplied string, every element of a collection, large buffers) lets an attacker inflate compute/log usage to make an instruction fail, or simply wastes the budget so legitimately-needed work no longer fits. Logging inside hot loops compounds the cost.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
pub fn record(ctx: Context<Record>, memo: String) -> Result<()> {
|
|
12
|
+
// Attacker-controlled, unbounded string logged verbatim; large memos
|
|
13
|
+
// burn compute and bloat logs, and can push the tx over its limits.
|
|
14
|
+
msg!("memo: {}", memo);
|
|
15
|
+
for item in ctx.accounts.list.items.iter() {
|
|
16
|
+
msg!("item: {:?}", item); // logging in a loop multiplies the cost
|
|
17
|
+
}
|
|
18
|
+
Ok(())
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Why this is dangerous
|
|
23
|
+
Excessive logging can exhaust the compute budget (causing the instruction to fail) and inflate transaction logs, degrading RPC/indexer performance for everyone reading the program's output. When a required instruction logs unbounded input, an attacker can deny its use; even absent an attacker, it raises costs and can intermittently break the path.
|
|
24
|
+
|
|
25
|
+
## Fix pattern
|
|
26
|
+
```rust
|
|
27
|
+
pub fn record(ctx: Context<Record>, memo: String) -> Result<()> {
|
|
28
|
+
require!(memo.len() <= MAX_MEMO_LEN, ErrorCode::MemoTooLong);
|
|
29
|
+
// Avoid logging raw user data; log bounded, structured summaries only.
|
|
30
|
+
msg!("record: memo_len={}", memo.len());
|
|
31
|
+
// Do not log inside large loops; emit a single summary if needed.
|
|
32
|
+
Ok(())
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
Prefer structured Anchor events (`emit!`) over verbose `msg!`, and never log full attacker-controlled buffers.
|
|
36
|
+
|
|
37
|
+
## Detection heuristic
|
|
38
|
+
- `msg!` formatting attacker-controlled strings/buffers without a length cap
|
|
39
|
+
- Logging inside loops over collections or `remaining_accounts`
|
|
40
|
+
- Verbose debug logging (`{:?}` on large structs) left in production paths
|
|
41
|
+
- No bound on user-supplied fields that are subsequently logged
|
|
42
|
+
|
|
43
|
+
## References
|
|
44
|
+
- Solana docs — program logging and limits (https://solana.com/docs/programs/debugging#logging)
|
|
45
|
+
- Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
|
|
46
|
+
- Sec3 — compute and log DoS (https://www.sec3.dev/blog)
|
|
47
|
+
|
|
48
|
+
## Real-world exploits (if any)
|
|
49
|
+
No single attributed public exploit; log/compute spam is a low-severity hygiene and availability finding in audits.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Rule 050: Stack / CPI Depth Exhaustion
|
|
2
|
+
|
|
3
|
+
**Severity:** Low
|
|
4
|
+
**Category:** Runtime
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
The Solana runtime caps cross-program invocation depth (CPI calls can nest only a limited number of levels) and each program has a bounded stack frame size (large stack-allocated structures/arrays can blow the frame). Designs that chain many CPIs, or that place large data on the stack (especially recursively or in deep call chains), can hit these limits and fail at runtime — bricking instructions that depend on the full chain.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
// Deeply nested CPI chain: A -> B -> C -> D ... approaching the CPI depth
|
|
12
|
+
// limit. If the chain needs one more hop than allowed, the whole flow fails.
|
|
13
|
+
pub fn route(ctx: Context<Route>) -> Result<()> {
|
|
14
|
+
invoke(&next_program_ix, accounts)?; // which itself CPIs further down
|
|
15
|
+
Ok(())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Large stack allocation inside a call that's already deep in a CPI chain:
|
|
19
|
+
let buffer = [0u8; 16_384]; // big stack frame, risks stack overflow
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Why this is dangerous
|
|
23
|
+
If an operation's success depends on a CPI chain near the depth limit, any addition (a new integration, a wrapper) tips it over and the instruction reverts — an availability failure that can freeze dependent funds. Oversized stack frames in deep chains can abort the program. Attackers may also craft inputs that force the deepest path.
|
|
24
|
+
|
|
25
|
+
## Fix pattern
|
|
26
|
+
```rust
|
|
27
|
+
// Keep CPI chains shallow; flatten orchestration into the top-level program
|
|
28
|
+
// rather than relaying through intermediate programs where avoidable.
|
|
29
|
+
// Move large data off the stack onto the heap or into accounts:
|
|
30
|
+
let buffer = vec![0u8; 16_384]; // heap-allocated; small stack frame
|
|
31
|
+
|
|
32
|
+
// Process multi-step flows as separate top-level instructions instead of
|
|
33
|
+
// one deep nested chain.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Detection heuristic
|
|
37
|
+
- CPI chains that relay through several intermediate programs (each adding a level)
|
|
38
|
+
- Large fixed-size arrays/structs allocated on the stack (`[u8; N]`, big local structs), especially in deep call paths or recursion
|
|
39
|
+
- Recursive program logic without a strict depth bound
|
|
40
|
+
- Designs assuming an arbitrary number of nested CPIs will succeed
|
|
41
|
+
|
|
42
|
+
## References
|
|
43
|
+
- Solana docs — CPI depth and stack limits (https://solana.com/docs/core/cpi)
|
|
44
|
+
- Solana docs — program runtime limits (https://docs.solanalabs.com/runtime/programming-model/runtime)
|
|
45
|
+
- Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
|
|
46
|
+
|
|
47
|
+
## Real-world exploits (if any)
|
|
48
|
+
No single attributed public exploit; depth/stack limits surface as availability/robustness findings in audits of programs with deep integration chains.
|
package/rules/INDEX.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Rule index
|
|
2
|
+
|
|
3
|
+
50 rules across 8 categories. Severity reflects typical impact when the pattern is present and exploitable; always judge in context.
|
|
4
|
+
|
|
5
|
+
| ID | Rule | Severity | Category |
|
|
6
|
+
|----|------|----------|----------|
|
|
7
|
+
| 001 | [Missing signer check](./001-missing-signer-check.md) | Critical | Account validation |
|
|
8
|
+
| 002 | [Missing owner check](./002-missing-owner-check.md) | Critical | Account validation |
|
|
9
|
+
| 003 | [Missing discriminator check (type cosplay)](./003-missing-discriminator-check.md) | High | Account validation |
|
|
10
|
+
| 004 | [Account substitution](./004-account-substitution.md) | High | Account validation |
|
|
11
|
+
| 005 | [Sysvar spoofing](./005-sysvar-spoofing.md) | High | Account validation |
|
|
12
|
+
| 006 | [Missing rent-exemption check](./006-missing-rent-exemption-check.md) | Low | Account validation |
|
|
13
|
+
| 007 | [Account aliasing (duplicate mutable accounts)](./007-account-aliasing.md) | High | Account validation |
|
|
14
|
+
| 008 | [Uninitialized account use](./008-uninitialized-account-use.md) | High | Account validation |
|
|
15
|
+
| 009 | [Missing `mut` constraint](./009-missing-mut-constraint.md) | Medium | Account validation |
|
|
16
|
+
| 010 | [Missing or improper close constraint](./010-missing-close-constraint.md) | Medium | Account validation |
|
|
17
|
+
| 011 | [PDA seed collision](./011-pda-seed-collision.md) | High | PDA |
|
|
18
|
+
| 012 | [Missing bump validation](./012-missing-bump-validation.md) | Medium | PDA |
|
|
19
|
+
| 013 | [Non-canonical bump accepted](./013-non-canonical-bump-accepted.md) | High | PDA |
|
|
20
|
+
| 014 | [Predictable / attacker-controlled PDA seeds](./014-predictable-pda.md) | High | PDA |
|
|
21
|
+
| 015 | [Insecure PDA layout across upgrades](./015-insecure-pda-across-upgrades.md) | Medium | PDA |
|
|
22
|
+
| 016 | [Stored bump mismatch](./016-bump-mismatch.md) | Medium | PDA |
|
|
23
|
+
| 017 | [Arbitrary CPI (unvalidated target)](./017-arbitrary-cpi.md) | Critical | CPI |
|
|
24
|
+
| 018 | [CPI confused deputy](./018-cpi-confused-deputy.md) | High | CPI |
|
|
25
|
+
| 019 | [Missing program ID check on SPL CPIs](./019-missing-program-id-check-spl.md) | High | CPI |
|
|
26
|
+
| 020 | [Reentrancy via CPI](./020-reentrancy-via-cpi.md) | High | CPI |
|
|
27
|
+
| 021 | [Untrusted callback execution](./021-untrusted-callback.md) | High | CPI |
|
|
28
|
+
| 022 | [CPI invoked with attacker-controlled accounts](./022-cpi-with-attacker-accounts.md) | High | CPI |
|
|
29
|
+
| 023 | [Lamport arithmetic overflow / underflow](./023-lamport-overflow.md) | High | Math |
|
|
30
|
+
| 024 | [Token amount arithmetic overflow](./024-token-amount-overflow.md) | High | Math |
|
|
31
|
+
| 025 | [Precision loss (division before multiplication)](./025-precision-loss.md) | Medium | Math |
|
|
32
|
+
| 026 | [Incorrect rounding direction](./026-rounding-direction.md) | Medium | Math |
|
|
33
|
+
| 027 | [Token decimal mismatch](./027-token-decimal-mismatch.md) | Medium | Math |
|
|
34
|
+
| 028 | [Integer cast truncation](./028-integer-cast-truncation.md) | Medium | Math |
|
|
35
|
+
| 029 | [Off-by-one errors](./029-off-by-one.md) | Low | Math |
|
|
36
|
+
| 030 | [Missing authorization on privileged instruction](./030-missing-authorization.md) | Critical | Auth |
|
|
37
|
+
| 031 | [Reinitialization attack](./031-reinitialization-attack.md) | High | Auth |
|
|
38
|
+
| 032 | [Closed account revival](./032-closed-account-revival.md) | High | Auth |
|
|
39
|
+
| 033 | [`init_if_needed` misuse](./033-init-if-needed-misuse.md) | High | Auth |
|
|
40
|
+
| 034 | [Missing `has_one` relationship enforcement](./034-missing-has-one.md) | High | Auth |
|
|
41
|
+
| 035 | [Insecure admin transfer (no acceptance handshake)](./035-insecure-admin-transfer.md) | Medium | Auth |
|
|
42
|
+
| 036 | [Missing pause / freeze guards](./036-missing-pause-guards.md) | Low | Auth |
|
|
43
|
+
| 037 | [Clock / time-based logic without bounds](./037-clock-manipulation.md) | Medium | Auth |
|
|
44
|
+
| 038 | [Missing `address` validation on fixed-identity accounts](./038-missing-address-validation.md) | Medium | Constraints |
|
|
45
|
+
| 039 | [Constraint evaluation stage (pre- vs post-state)](./039-constraint-evaluation-stage.md) | Medium | Constraints |
|
|
46
|
+
| 040 | [`realloc` without zero-init](./040-realloc-zero-init.md) | Medium | Constraints |
|
|
47
|
+
| 041 | [`init` without `payer` (or wrong payer)](./041-missing-payer-on-init.md) | Low | Constraints |
|
|
48
|
+
| 042 | [Incorrect `space` allocation](./042-incorrect-space-allocation.md) | Medium | Constraints |
|
|
49
|
+
| 043 | [`Account` vs `AccountInfo` misuse](./043-account-vs-account-info.md) | High | Constraints |
|
|
50
|
+
| 044 | [Token account owner unverified](./044-token-account-owner-unverified.md) | High | SPL Token |
|
|
51
|
+
| 045 | [Token mint unverified](./045-token-mint-unverified.md) | High | SPL Token |
|
|
52
|
+
| 046 | [Associated token account assumption errors](./046-ata-assumption-errors.md) | Medium | SPL Token |
|
|
53
|
+
| 047 | [Token program ID hardcoded vs. validated](./047-token-program-id-hardcoded.md) | Medium | SPL Token |
|
|
54
|
+
| 048 | [Compute budget abuse (unbounded work)](./048-compute-budget-abuse.md) | Medium | Runtime |
|
|
55
|
+
| 049 | [Log spam / excessive logging DoS](./049-log-spam-dos.md) | Low | Runtime |
|
|
56
|
+
| 050 | [Stack / CPI depth exhaustion](./050-stack-overflow-deep-cpi.md) | Low | Runtime |
|
|
57
|
+
|
|
58
|
+
## Severity counts
|
|
59
|
+
|
|
60
|
+
| Severity | Count |
|
|
61
|
+
|----------|-------|
|
|
62
|
+
| Critical | 4 |
|
|
63
|
+
| High | 21 |
|
|
64
|
+
| Medium | 17 |
|
|
65
|
+
| Low | 8 |
|
package/rules/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Rule catalog
|
|
2
|
+
|
|
3
|
+
One markdown file per rule, named `NNN-kebab-case-name.md` (e.g. `001-missing-signer-check.md`). The full list with severities lives in [INDEX.md](./INDEX.md).
|
|
4
|
+
|
|
5
|
+
The catalog is consumed by both the Claude Code skill ([SKILL.md](../SKILL.md)) and the CLI (`cli/src/rules-loader.ts`), so every file must follow this exact template:
|
|
6
|
+
|
|
7
|
+
````markdown
|
|
8
|
+
# Rule NNN: <Title>
|
|
9
|
+
|
|
10
|
+
**Severity:** Critical | High | Medium | Low
|
|
11
|
+
**Category:** Account validation | PDA | CPI | Math | Auth | Constraints | SPL Token | Runtime
|
|
12
|
+
|
|
13
|
+
## Description
|
|
14
|
+
One paragraph explaining the vulnerability and why it matters.
|
|
15
|
+
|
|
16
|
+
## Vulnerable pattern
|
|
17
|
+
```rust
|
|
18
|
+
// Minimal Rust/Anchor snippet showing the bug
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Why this is dangerous
|
|
22
|
+
Explain attacker action and impact in 2 to 4 sentences.
|
|
23
|
+
|
|
24
|
+
## Fix pattern
|
|
25
|
+
```rust
|
|
26
|
+
// Minimal Rust/Anchor snippet showing the corrected code
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Detection heuristic
|
|
30
|
+
Bullet list of things an agent should look for in source code to flag this rule.
|
|
31
|
+
|
|
32
|
+
## References
|
|
33
|
+
- Source 1 (URL)
|
|
34
|
+
- Source 2 (URL)
|
|
35
|
+
|
|
36
|
+
## Real-world exploits (if any)
|
|
37
|
+
Optional: brief mention of public exploits matching this pattern.
|
|
38
|
+
````
|
|
39
|
+
|
|
40
|
+
Content must be sourced (Neodyme, Sec3, Helius, Solana Cookbook, Cyfrin Updraft, the Anchor book, public audit reports) and cited in the References section — never invented.
|