Writing Custom Semgrep Rules to Catch Solana Anchor Vulnerabilities Before Auditors Do
You've written your Anchor program. It compiles. Tests pass. You're ready to deploy.
But buried in your instruction handlers are patterns that every experienced auditor knows to look for — and that automated tools regularly miss. Missing signer checks. Unconstrained account deserialization. Authority fields that nobody validates.
Here's the thing: you don't need to wait for a $50K audit to find these. With Semgrep — a lightweight static analysis tool — you can write custom rules that catch the most common Anchor vulnerabilities in seconds.
This guide walks through building a practical Semgrep ruleset for Solana Anchor programs. Every rule targets a real vulnerability class that has led to actual exploits.
Why Semgrep for Solana?
The Solana security tooling landscape in 2026 looks like this:
| Tool | Strength | Weakness |
|---|---|---|
| cargo-audit | Known CVEs in dependencies | Zero coverage of logic bugs |
| Soteria | Solana-specific checks | Abandoned/limited maintenance |
| Anchor's built-in checks | Automatic with proper types | Only works if you use them correctly |
| Semgrep | Custom rules, fast, CI-friendly | You have to write the rules yourself |
Semgrep fills a critical gap: it lets you encode project-specific security invariants as rules that run in CI. Unlike generic linters, you're matching actual code patterns — not just style violations.
Setup
pip install semgrep
mkdir -p .semgrep/
That's it. No Rust toolchain dependency, no compilation step. Rules are YAML files.
Rule 1: Missing Signer Check on Authority Accounts
The vulnerability: An instruction accepts an authority account but doesn't verify it actually signed the transaction. An attacker can pass any public key as the authority.
Real-world impact: The Wormhole exploit ($320M) stemmed from missing signature verification.
# .semgrep/missing-signer-check.yaml
rules:
- id: anchor-missing-signer-on-authority
patterns:
- pattern: |
pub $FIELD: AccountInfo<'info>
- metavariable-regex:
metavariable: $FIELD
regex: "(authority|admin|owner|signer|payer|creator)"
- pattern-not: |
#[account(signer)]
pub $FIELD: AccountInfo<'info>
- pattern-not: |
pub $FIELD: Signer<'info>
message: >
Account '$FIELD' looks like an authority but has no signer constraint.
Use `Signer<'info>` type or add `#[account(signer)]` constraint.
Missing signer checks allow anyone to impersonate the authority.
severity: ERROR
languages: [rust]
metadata:
category: security
impact: CRITICAL
cwe: "CWE-862: Missing Authorization"
references:
- https://secure-contracts.com/not-so-smart-contracts/solana/signer_check/
What it catches:
// ❌ VULNERABLE — no signer verification
#[derive(Accounts)]
pub struct WithdrawFunds<'info> {
#[account(mut)]
pub vault: Account<'info, Vault>,
pub authority: AccountInfo<'info>, // Semgrep flags this
}
// ✅ SAFE — Signer type enforces signature check
#[derive(Accounts)]
pub struct WithdrawFunds<'info> {
#[account(mut)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>,
}
Rule 2: Unchecked Account Ownership
The vulnerability: Using AccountInfo instead of Account<'info, T> means Anchor skips owner and discriminator validation. An attacker can pass a fake account with crafted data.
# .semgrep/unchecked-account-deserialization.yaml
rules:
- id: anchor-unchecked-account-info-deserialization
patterns:
- pattern: |
let $DATA = $ACCOUNT.try_borrow_data()?;
- pattern-inside: |
pub fn $FUNC(ctx: Context<$ACCOUNTS>, ...) -> Result<()> {
...
}
- pattern-not-inside: |
if $ACCOUNT.owner != $PROGRAM_ID { ... }
message: >
Raw deserialization of AccountInfo data without owner check.
If '$ACCOUNT' is an AccountInfo (not Account<T>), an attacker can
pass a fake account owned by any program. Verify account.owner
or use Account<'info, T> for automatic validation.
severity: WARNING
languages: [rust]
metadata:
category: security
impact: HIGH
cwe: "CWE-345: Insufficient Verification of Data Authenticity"
Rule 3: PDA Seeds Without Bump Validation
The vulnerability: Creating or validating PDAs without specifying the canonical bump allows attackers to use non-canonical bumps, potentially creating duplicate accounts.
# .semgrep/pda-missing-bump.yaml
rules:
- id: anchor-pda-missing-bump-constraint
patterns:
- pattern: |
#[account(
seeds = [$...SEEDS],
$...REST
)]
- pattern-not: |
#[account(
seeds = [$...SEEDS],
bump = $BUMP,
$...REST
)]
- pattern-not: |
#[account(
seeds = [$...SEEDS],
bump,
$...REST
)]
message: >
PDA account uses seeds constraint without explicit bump.
Always include `bump` (or `bump = field_name`) to enforce
the canonical bump. Without this, an attacker might find a
valid non-canonical bump to bypass account derivation checks.
severity: WARNING
languages: [rust]
metadata:
category: security
impact: MEDIUM
cwe: "CWE-330: Use of Insufficiently Random Values"
Rule 4: Arithmetic Without Overflow Protection
The vulnerability: Using +, -, * operators on token amounts without checked_ math. In release builds without overflow checks, this wraps silently.
# .semgrep/unchecked-arithmetic.yaml
rules:
- id: anchor-unchecked-arithmetic-on-amounts
patterns:
- pattern-either:
- pattern: $A + $B
- pattern: $A - $B
- pattern: $A * $B
- metavariable-regex:
metavariable: $A
regex: ".*(amount|balance|supply|deposit|withdraw|fee|reward|lamports).*"
- pattern-not-inside: |
$A.checked_add($B)
- pattern-not-inside: |
$A.checked_sub($B)
- pattern-not-inside: |
$A.checked_mul($B)
message: >
Arithmetic operation on what looks like a financial amount
without using checked_add/checked_sub/checked_mul. This can
silently overflow in release builds. Use checked math or
Anchor's require!() to validate bounds.
severity: WARNING
languages: [rust]
metadata:
category: security
impact: HIGH
cwe: "CWE-190: Integer Overflow or Wraparound"
Rule 5: Missing close Constraint Leaves Lamports Locked
The vulnerability: Accounts that should be closeable don't have the close constraint, leaving lamports permanently locked in the account.
# .semgrep/missing-close-constraint.yaml
rules:
- id: anchor-missing-close-on-ephemeral-account
patterns:
- pattern: |
#[account(
init,
$...REST
)]
pub $FIELD: Account<'info, $TYPE>
- metavariable-regex:
metavariable: $TYPE
regex: "(Escrow|Order|Bid|Offer|Request|Ticket|Receipt)"
message: >
Account type '$TYPE' looks ephemeral but is initialized without
a corresponding close instruction. Consider adding a close
instruction with `#[account(mut, close = destination)]` to
allow reclaiming lamports when the account is no longer needed.
severity: INFO
languages: [rust]
metadata:
category: security
impact: LOW
Putting It All Together: CI Integration
Create a .semgrep.yml config at your project root:
# .semgrep.yml
rules:
- .semgrep/
Then add to your CI pipeline:
# .github/workflows/security.yml
name: Solana Security Scan
on: [push, pull_request]
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: .semgrep/
Every PR now gets checked against your custom security rules before any human reviews it.
What This Doesn't Catch
Let's be honest about limitations:
- Cross-instruction logic bugs — Semgrep is intra-file; it can't trace state across CPIs
- Economic exploits — Flash loan attacks, oracle manipulation, MEV — these need runtime simulation
- Timing/ordering issues — Race conditions in concurrent transaction processing
For these, you need dynamic analysis tools like Trident (fuzzing) or Guardrail (runtime monitoring). Static analysis is your first line of defense, not your only one.
The Takeaway
The best time to catch a vulnerability is before deployment. The second best time is during code review. Both are cheaper than a post-exploit audit.
These five Semgrep rules won't replace a professional audit — but they'll catch the low-hanging fruit that accounts for ~40% of Solana exploits. And unlike a one-time audit, they run on every commit, forever.
The full ruleset is available as a GitHub Gist — fork it, customize it for your project, and make it part of your CI pipeline.
Building security tooling for Solana? I write weekly about DeFi security engineering, audit methodologies, and vulnerability research. Follow for more.
Top comments (0)