Token Account Ownership
Detects missing account ownership validation before cross-program invocations, allowing attackers to pass fake token accounts.
Token Account Ownership
Overview
Remediation Guide: How to Fix Token Account Ownership Issues
The token account ownership detector identifies Solana programs that pass accounts into cross-program invocations without verifying that the accounts are owned by the expected program. When an account is used in a CPI (especially token operations), the calling program must verify that the account is owned by the Token program or the expected program. Without this check, an attacker can pass a fake account owned by a malicious program that returns crafted data.
Sigvex performs CFG-aware dataflow analysis, tracking validation states (signer, owner, key) across basic blocks using shared dataflow state. Accounts used in CPI calls that lack owner validation generate findings. CWE mapping: CWE-862 (Missing Authorization).
Why This Is an Issue
In Solana’s account model, any account can be passed to any instruction. The program must validate that each account is what it claims to be. For token operations specifically:
- Fake token accounts: An attacker creates an account with crafted data mimicking a token account but owned by their own program, bypassing balance checks.
- Wrong mint: Without owner validation, an account from a different token mint can be substituted, mixing token types.
- Authority spoofing: A fake authority account can authorize operations on real token accounts if the program does not verify account ownership.
How to Resolve
Native Rust
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
const TOKEN_PROGRAM_ID: Pubkey = spl_token::id();
pub fn transfer_tokens(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let source = &accounts[0];
let destination = &accounts[1];
let authority = &accounts[2];
let token_program = &accounts[3];
// Validate ownership of token accounts
if source.owner != &TOKEN_PROGRAM_ID {
return Err(ProgramError::IllegalOwner);
}
if destination.owner != &TOKEN_PROGRAM_ID {
return Err(ProgramError::IllegalOwner);
}
// Validate signer
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Safe to CPI
spl_token::instruction::transfer(
&TOKEN_PROGRAM_ID, source.key, destination.key,
authority.key, &[], amount,
)?;
Ok(())
}
Anchor
#[derive(Accounts)]
pub struct TransferTokens<'info> {
// Account<'info, TokenAccount> validates ownership by SPL Token program
#[account(mut)]
pub source: Account<'info, TokenAccount>,
#[account(mut)]
pub destination: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
Examples
Vulnerable Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn stake_tokens(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let user_token = &accounts[0]; // No owner check!
let pool_token = &accounts[1]; // No owner check!
let authority = &accounts[2];
// CPI with unvalidated accounts -- attacker passes fake token accounts
solana_program::program::invoke(
&spl_token::instruction::transfer(
&spl_token::id(), user_token.key, pool_token.key,
authority.key, &[], amount,
)?,
&[user_token.clone(), pool_token.clone(), authority.clone()],
)?;
Ok(())
}
Fixed Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn stake_tokens(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let user_token = &accounts[0];
let pool_token = &accounts[1];
let authority = &accounts[2];
// Validate token account ownership
if user_token.owner != &spl_token::id() || pool_token.owner != &spl_token::id() {
return Err(ProgramError::IllegalOwner);
}
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
solana_program::program::invoke(
&spl_token::instruction::transfer(
&spl_token::id(), user_token.key, pool_token.key,
authority.key, &[], amount,
)?,
&[user_token.clone(), pool_token.clone(), authority.clone()],
)?;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "token-account-ownership",
"severity": "high",
"confidence": 0.82,
"description": "Account v0 is passed to a CPI without owner validation. An attacker can pass a fake account owned by a malicious program to bypass token validation checks.",
"location": { "function": "stake_tokens", "offset": 3 }
}
Detection Methodology
The detector uses CFG-aware dataflow analysis:
- Dataflow state propagation: Validation states (signer check, owner check, key check) and variable aliases are tracked through the control-flow graph, propagating from predecessors to successors.
- CPI account inspection: At each CPI site, the detector checks whether accounts passed to the invocation have been owner-validated according to the propagated state.
- Alias tracking: Variable assignments that alias one variable to another propagate validation state through aliases.
Context modifiers:
- Anchor programs: confidence reduced (Anchor’s
Account<T>validates ownership) - Admin/initialization functions: confidence reduced by 40%
Limitations
False positives:
- Programs where the CPI target itself validates account ownership (e.g., SPL Token program validates token account ownership internally) may produce findings that are mitigated at the CPI level.
- Accounts validated through PDA derivation (which implicitly proves ownership) may be flagged.
False negatives:
- Ownership validation performed through helper functions or macros is not tracked through the dataflow analysis.
- Accounts passed through complex expressions (nested structs, arrays) may not be traced to their validation sites.
Related Detectors
- SPL Token Account Validation — full SPL token operation validation
- Missing Owner Check — general missing owner validation