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.
Recommended Fix
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
1. Anchor #[account] macro (recommended)
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 Anchor'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>,
}