Readonly CPI Write Bypass
Detects readonly accounts forwarded to CPI calls that may bypass write locks.
Readonly CPI Write Bypass
Overview
The readonly CPI write bypass detector identifies when accounts received as readonly are passed into CPI calls where the target program could modify them. If the CPI target program owns the account, it can write to it regardless of the caller’s readonly designation, bypassing the caller’s expected access control.
For remediation guidance, see Readonly CPI Write Bypass Remediation.
Why This Is an Issue
Program A receives account X as readonly, assuming X cannot be modified during the instruction. Program A then passes X in a CPI to Program B. If Program B owns account X, it can modify X freely. When Program A continues execution after the CPI, it may read stale data from X, not realizing the CPI changed it. This is an indirect write lock bypass that can lead to state inconsistencies and security vulnerabilities.
How to Resolve
Before (Vulnerable)
// Vulnerable: readonly account forwarded to CPI
pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
let readonly_account = &accounts[0]; // Not writable
let external_program = &accounts[1];
// CPI passes readonly_account -- target may modify it
invoke(&instruction, &[readonly_account.clone(), external_program.clone()])?;
// readonly_account data may have changed
Ok(())
}
After (Fixed)
// Fixed: verify writability or reload after CPI
pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
let account = &accounts[0];
let external_program = &accounts[1];
require!(account.is_writable, ProgramError::InvalidArgument);
let data_before = account.data.borrow().to_vec();
invoke(&instruction, &[account.clone(), external_program.clone()])?;
// Validate state after CPI
let data_after = account.data.borrow();
if data_before != *data_after {
msg!("Account modified by CPI -- re-validating");
validate_account_state(account)?;
}
Ok(())
}
Example JSON Finding
{
"detector": "readonly-cpi-write-bypass",
"severity": "high",
"confidence": 0.65,
"message": "Readonly account forwarded to CPI without writability verification",
"location": { "function": "process", "block": 0, "statement": 3 }
}
Detection Methodology
- Writability check collection: Tracks which accounts have
CheckWritablevalidation. - CPI account forwarding: Identifies CPI calls that include accounts not verified as writable.
- Post-CPI state usage: Flags code that reads account data after CPI without re-validation.
Limitations
False positives: CPI to programs that cannot modify the forwarded account (not the owner). False negatives: Write bypass through multiple levels of CPI indirection.
Related Detectors
- Cross-Program State — state inconsistencies across CPI
- Anchor Constraint TOCTOU — TOCTOU after CPI