Skip to main content
Sigvex

Type Cosplay Remediation

How to prevent type cosplay attacks by validating the account discriminator before deserializing account data.

Type Cosplay Remediation

Overview

Related Detector: Type Cosplay

Type cosplay attacks exploit programs that read or write account data without validating the discriminator prefix, allowing attackers to pass accounts of a different type. The primary fix is to always validate the first 8 bytes (Anchor) or custom discriminator of an account before accessing its fields.

Before (Vulnerable)

// Native program — no discriminator check
pub fn process_staking(accounts: &[AccountInfo]) -> ProgramResult {
    let staker_info = &accounts[0];
    let data = staker_info.data.borrow();

    // VULNERABLE: reading stake amount at offset 8 without checking discriminator
    // Attacker passes a Vault account whose bytes 8-16 contain a large value
    let stake_amount = u64::from_le_bytes(data[8..16].try_into().unwrap());
    let authority = Pubkey::from_slice(&data[16..48])?;

    // stake_amount and authority could be attacker-controlled via type cosplay
    process_rewards(authority, stake_amount)?;
    Ok(())
}

After (Fixed)

const STAKER_INFO_DISCRIMINATOR: [u8; 8] = *b"stakeacc";

pub fn process_staking(accounts: &[AccountInfo]) -> ProgramResult {
    let staker_info = &accounts[0];
    let data = staker_info.data.borrow();

    // FIXED: verify account type before reading fields
    if data.len() < 8 {
        return Err(ProgramError::InvalidAccountData);
    }
    if data[0..8] != STAKER_INFO_DISCRIMINATOR {
        return Err(ProgramError::InvalidAccountData);
    }

    let stake_amount = u64::from_le_bytes(data[8..16].try_into().unwrap());
    let authority = Pubkey::from_slice(&data[16..48])?;
    process_rewards(authority, stake_amount)?;
    Ok(())
}

The discriminator check ensures the account contains StakerInfo data, not a Vault or other type whose bytes happen to look valid.

Alternative Mitigations

Anchor computes and validates the 8-byte SHA256 discriminator automatically:

use anchor_lang::prelude::*;

// Anchor generates a unique 8-byte discriminator for StakerInfo
// based on SHA256("account:StakerInfo")[0..8]
#[account]
pub struct StakerInfo {
    pub authority: Pubkey,     // offset 8
    pub stake_amount: u64,     // offset 40
    pub rewards_earned: u64,   // offset 48
    pub last_stake_time: i64,  // offset 56
}

#[derive(Accounts)]
pub struct ProcessStaking<'info> {
    // Account<'info, StakerInfo>:
    // 1. Validates account.owner == program_id
    // 2. Validates discriminator bytes == SHA256("account:StakerInfo")[0..8]
    // 3. Deserializes the struct safely
    pub staker_info: Account<'info, StakerInfo>,
}

pub fn process_staking(ctx: Context<ProcessStaking>) -> Result<()> {
    let staker = &ctx.accounts.staker_info;
    process_rewards(staker.authority, staker.stake_amount)?;
    Ok(())
}

2. Custom discriminator trait for native programs

For native programs, implement a discriminator validation trait:

use std::convert::TryFrom;

pub trait Discriminated: Sized {
    const DISCRIMINATOR: [u8; 8];

    fn from_account_data(data: &[u8]) -> Result<Self, ProgramError>
    where
        Self: BorshDeserialize,
    {
        if data.len() < 8 {
            return Err(ProgramError::InvalidAccountData);
        }
        if data[..8] != Self::DISCRIMINATOR {
            return Err(ProgramError::InvalidAccountData);
        }
        Self::try_from_slice(&data[8..]).map_err(|_| ProgramError::InvalidAccountData)
    }
}

#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakerInfo {
    pub authority: Pubkey,
    pub stake_amount: u64,
}

impl Discriminated for StakerInfo {
    const DISCRIMINATOR: [u8; 8] = *b"stakeacc";
}

// Usage:
pub fn process_staking(accounts: &[AccountInfo]) -> ProgramResult {
    let data = accounts[0].data.borrow();
    let staker = StakerInfo::from_account_data(&data)?; // Validates discriminator
    process_rewards(staker.authority, staker.stake_amount)?;
    Ok(())
}

Common Mistakes

Mistake 1: Owner check instead of discriminator check

// INSUFFICIENT: owner check prevents cross-program attacks but not
// intra-program type cosplay (two account types from the same program)
if accounts[0].owner != &crate::id() {
    return Err(ProgramError::IncorrectProgramId);
}
// Still vulnerable: attacker passes a Vault account (same program owner)
// whose bytes match StakerInfo layout
let stake_amount = u64::from_le_bytes(data[8..16].try_into().unwrap());

Owner checks are necessary but not sufficient — always also check the discriminator.

Mistake 2: Checking discriminator only for writes, not reads

// INCOMPLETE: checks discriminator for writes but not reads
pub fn update_authority(accounts: &[AccountInfo], new_auth: Pubkey) -> ProgramResult {
    let data = accounts[0].data.borrow();
    let current_auth = Pubkey::from_slice(&data[8..40])?; // Read without discriminator check

    let mut write_data = accounts[0].data.borrow_mut();
    if write_data[0..8] != DISCRIMINATOR {  // Only checks on write
        return Err(ProgramError::InvalidAccountData);
    }
    write_data[8..40].copy_from_slice(new_auth.as_ref());
    Ok(())
}

Validate the discriminator before any data access, whether read or write.

Mistake 3: Using AccountInfo instead of Account<T> in Anchor

// WRONG: UncheckedAccount and AccountInfo bypass the account-validation framework's discriminator check
#[derive(Accounts)]
pub struct VulnerableCtx<'info> {
    pub staker_info: UncheckedAccount<'info>,  // No discriminator check!
    // Should be: pub staker_info: Account<'info, StakerInfo>,
}

References