Skip to main content
Sigvex

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

  1. Writability check collection: Tracks which accounts have CheckWritable validation.
  2. CPI account forwarding: Identifies CPI calls that include accounts not verified as writable.
  3. 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.

References