Account Owner Chain Remediation
How to fix stale owner validation across CPI boundaries.
Remediating Account Owner Chain Issues
Overview
Related Detector: Account Owner Chain
After a cross-program invocation, any pre-CPI owner validation is stale because the invoked program may have modified the account. The fix is to re-validate account ownership after every CPI before accessing the account again.
Recommended Fix
Before (Vulnerable)
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
let account = &accounts[0];
// Owner check
if account.owner != &MY_PROGRAM_ID {
return Err(ProgramError::IllegalOwner);
}
// CPI
solana_program::program::invoke(&instruction, &[account.clone()])?;
// Access without re-validation -- VULNERABLE
let data = account.data.borrow();
Ok(())
}
After (Fixed)
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
let account = &accounts[0];
if account.owner != &MY_PROGRAM_ID {
return Err(ProgramError::IllegalOwner);
}
solana_program::program::invoke(&instruction, &[account.clone()])?;
// Re-validate after CPI
if account.owner != &MY_PROGRAM_ID {
return Err(ProgramError::IllegalOwner);
}
let data = account.data.borrow();
Ok(())
}
Alternative Mitigations
- Access before CPI only: If possible, read all necessary data from the account before the CPI and do not access it afterward. This avoids the TOCTOU window entirely.
let data = account.data.borrow().to_vec(); // Read before CPI
drop(account.data.borrow());
solana_program::program::invoke(&instruction, &[account.clone()])?;
// Use 'data' snapshot -- no post-CPI access needed
-
Use read-only accounts in CPI: If the invoked program does not need to modify the account, pass it as non-writable. This prevents ownership changes during the CPI.
-
Validate multiple properties after CPI: Re-check not just ownership but also discriminator, data length, and key to ensure the account has not been tampered with.
// Full post-CPI validation
if account.owner != &MY_PROGRAM_ID {
return Err(ProgramError::IllegalOwner);
}
if account.data.borrow()[0..8] != EXPECTED_DISCRIMINATOR {
return Err(ErrorCode::InvalidDiscriminator.into());
}
Common Mistakes
- Re-validating only one account when multiple are passed to CPI: All accounts that are accessed after a CPI must be re-validated, not just the primary account.
- Re-validating against a variable that could have been modified: Re-check against a constant or a trusted PDA, not against a value read from another account that was also passed to the CPI.
- Forgetting re-validation between consecutive CPIs: Each CPI creates a new trust boundary. If you make two CPIs, re-validate between them.
- Assuming Anchor handles post-CPI validation: Anchor validates accounts at instruction entry but does not automatically re-validate after CPI calls within the handler. You must add explicit checks.