Skip to main content
Sigvex

Missing Writable Check Remediation

How to add is_writable validation before account modifications in Solana native programs and use the account-validation framework's #[account(mut)] constraint.

Missing Writable Check Remediation

Overview

Missing writable check vulnerabilities arise when a Solana program writes to account data or transfers lamports without verifying the is_writable flag. The remediation is to add an explicit is_writable check before any modification in native programs, or to use the #[account(mut)] constraint in Anchor which validates is_writable automatically during account deserialization.

Related Detector: Missing Writable Check

Before (Vulnerable)

use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};

pub fn update_state(accounts: &[AccountInfo], value: u64) -> ProgramResult {
    let account = &accounts[0];
    // VULNERABLE: writes without checking is_writable
    let mut data = account.data.borrow_mut();
    data[0..8].copy_from_slice(&value.to_le_bytes());
    Ok(())
}

After (Fixed)

use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    program_error::ProgramError,
};

pub fn update_state(accounts: &[AccountInfo], value: u64) -> ProgramResult {
    let account = &accounts[0];

    // FIXED: validate is_writable before any modification
    if !account.is_writable {
        return Err(ProgramError::InvalidAccountData);
    }

    let mut data = account.data.borrow_mut();
    data[0..8].copy_from_slice(&value.to_le_bytes());
    Ok(())
}

Alternative Mitigations

Anchor #[account(mut)] — automatically validates is_writable during deserialization:

use anchor_lang::prelude::*;

#[account]
pub struct State {
    pub value: u64,
}

#[derive(Accounts)]
pub struct UpdateState<'info> {
    // mut constraint validates is_writable — no manual check needed
    #[account(mut)]
    pub state: Account<'info, State>,
    pub authority: Signer<'info>,
}

pub fn update_state(ctx: Context<UpdateState>, value: u64) -> Result<()> {
    ctx.accounts.state.value = value;
    Ok(())
}

Centralized account validation function — for native programs with many instructions, validate all account flags in one place:

pub fn validate_accounts(accounts: &[AccountInfo]) -> ProgramResult {
    let writable_account = &accounts[0];
    let signer_account = &accounts[1];

    if !writable_account.is_writable {
        return Err(ProgramError::InvalidAccountData);
    }
    if !signer_account.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    Ok(())
}

Common Mistakes

Skipping the check for “internal” accounts — even accounts that the program itself created may be passed as read-only by a caller. Always check is_writable regardless of expected account origin.

Checking is_writable after the borrow — the check must come before data.borrow_mut(). Once the mutable borrow is attempted, the panic or error has already occurred.

Assuming CPI propagates writability — when invoking another program via CPI, the called program enforces its own writability requirements. The calling program must still validate writability before any direct writes it performs.

References