Type Cosplay (Discriminator Spoofing)
Detects account data deserialization that reads past the discriminator prefix without first validating it, allowing attackers to pass accounts of a different type.
Type Cosplay (Discriminator Spoofing)
Overview
Remediation Guide: How to Fix Type Cosplay
The type cosplay detector identifies Solana programs that read or write account data past the discriminator region (offset > 0) without first comparing the discriminator bytes at offset 0. Anchor programs use an 8-byte SHA256-based discriminator at the beginning of account data to identify the account type. Non-Anchor programs may use their own type-tag scheme. If a program reads fields from an account without first verifying the discriminator, an attacker can craft an account whose raw bytes happen to match the expected data layout of a different account type, causing the program to misinterpret fields — the “type cosplay” attack.
Sigvex performs a two-pass analysis: (1) collect all reads and branches that compare discriminator-region bytes, and (2) flag any data access past offset 0 where no discriminator validation was found on any control-flow path leading to that access.
Why This Is an Issue
Type cosplay allows privilege escalation by spoofing high-privilege account types. An attacker can create a token account whose bytes, when interpreted as an admin account, shows the attacker as the program authority. Without a discriminator check, the program trusts the crafted account as a legitimate admin record and grants the attacker elevated privileges.
This class of vulnerability is well-documented in the Solana security community and is one reason Anchor’s Account<'info, T> — which validates the discriminator at deserialization — is strongly recommended over raw AccountInfo.
CWE mapping: CWE-843 (Access of Resource Using Incompatible Type).
How to Resolve
// Before: Vulnerable — reads account data without discriminator check
pub fn update_admin(accounts: &[AccountInfo]) -> ProgramResult {
let config_account = &accounts[0];
let data = config_account.data.borrow();
// VULNERABLE: reading at offset 8+ without checking offset 0
let admin_pubkey = Pubkey::from_slice(&data[8..40])?;
// Attacker creates a token account whose bytes 8-40 contain their own pubkey
if admin_pubkey != *accounts[1].key {
return Err(ProgramError::InvalidAccountData);
}
// Proceeds with attacker as admin...
Ok(())
}
// After: Fixed — validate discriminator first
const CONFIG_DISCRIMINATOR: [u8; 8] = [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0];
pub fn update_admin(accounts: &[AccountInfo]) -> ProgramResult {
let config_account = &accounts[0];
let data = config_account.data.borrow();
// FIXED: check discriminator first
if data[..8] != CONFIG_DISCRIMINATOR {
return Err(ProgramError::InvalidAccountData);
}
let admin_pubkey = Pubkey::from_slice(&data[8..40])?;
if admin_pubkey != *accounts[1].key {
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
For Anchor (recommended approach):
// Anchor auto-validates the 8-byte discriminator for Account<T>
#[account]
pub struct Config {
pub admin: Pubkey,
pub fee_rate: u64,
}
#[derive(Accounts)]
pub struct UpdateAdmin<'info> {
// Account<'info, Config> automatically validates:
// 1. The account is owned by this program
// 2. The first 8 bytes match Config's discriminator
#[account(mut)]
pub config: Account<'info, Config>,
#[account(signer)]
pub current_admin: Signer<'info>,
}
Examples
Vulnerable Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
// Native program — reads account data without type validation
pub fn process_vault_operation(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let vault = &accounts[0];
let data = vault.data.borrow();
// VULNERABLE: Reads vault fields without checking that this is actually a Vault account
// An attacker could pass a Staking account whose bytes at offsets 0-32 look like
// a vault authority, granting unauthorized access
let authority = Pubkey::from_slice(&data[0..32])?;
let balance = u64::from_le_bytes(data[32..40].try_into().unwrap());
// Trusts the "authority" field without knowing the account type
if authority == *accounts[1].key {
**vault.lamports.borrow_mut() -= amount;
}
Ok(())
}
Fixed Code
const VAULT_DISCRIMINATOR: [u8; 8] = *b"vaultacc";
pub fn process_vault_operation(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let vault = &accounts[0];
let data = vault.data.borrow();
// FIXED: validate account type before reading fields
require!(
data[..8] == VAULT_DISCRIMINATOR,
MyError::InvalidAccountType
);
let authority = Pubkey::from_slice(&data[8..40])?;
let balance = u64::from_le_bytes(data[40..48].try_into().unwrap());
if authority == *accounts[1].key {
**vault.lamports.borrow_mut() -= amount;
}
Ok(())
}
Sample Sigvex Output
{
"detector_id": "type-cosplay",
"severity": "high",
"confidence": 0.60,
"description": "Account v0 data is read at an offset past the discriminator region without first validating the discriminator bytes at offset 0. An attacker could pass an account of a different type whose data layout happens to deserialize successfully.",
"location": { "function": "process_vault_operation", "offset": 3 }
}
Detection Methodology
The detector performs two passes over the function’s HIR:
Pass 1 — Discriminator validation collection:
- Identifies
HirStmt::Assignwhere source isHirExpr::AccountDatawith offset 0 and sizeByteorDouble— these are discriminator reads. - Tracks the result variable into a
discriminator_read_varsset. - Scans branch conditions (
HirStmt::Branchterminators) for equality comparisons (BinOp::EqorBinOp::Ne) of discriminator-read variables against constants. CheckKeystatements also mark accounts as validated (PDA key implies type safety).
Pass 2 — Violation detection:
- Scans all
HirStmt::AssignwithHirExpr::AccountDataat offset > 0 for accounts not in the validated set. - Scans all
HirStmt::StoreAccountDataat offset > 0 for accounts not validated.
Read findings (severity Medium, confidence 0.60): Data read past discriminator without validation. Write findings (severity High, confidence 0.65): Data written past discriminator without validation.
Context modifiers: Anchor programs with discriminator validation reduce confidence by 0.15; without by 0.30. PDA-derived accounts reduce by 0.40. Read-only functions reduce by 0.40.
Limitations
False positives:
- Anchor
Account<'info, T>types perform automatic discriminator validation — programs using Anchor correctly receive significantly reduced confidence (0.15x with confirmed discriminator checks). - Native programs that perform type validation through an owner check followed by program-specific data verification may still be flagged since the pattern may not match the detector’s discriminator-read pattern.
False negatives:
- Discriminator validation performed inside a called function (not inlined in the current function’s IR) is not detected.
- Programs that use a 4-byte discriminator instead of the standard 8-byte Anchor discriminator may not be recognized.
Related Detectors
- Missing Owner Check — validates account program ownership
- Account Reinitialization — detects reinitialization attacks on accounts without discriminator guards