Missing Writable Check Remediation
How to add is_writable validation before account modifications in Solana native programs and use Anchor'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
Recommended Fix
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.