Skip to main content
Sigvex

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::Assign where source is HirExpr::AccountData with offset 0 and size Byte or Double — these are discriminator reads.
  • Tracks the result variable into a discriminator_read_vars set.
  • Scans branch conditions (HirStmt::Branch terminators) for equality comparisons (BinOp::Eq or BinOp::Ne) of discriminator-read variables against constants.
  • CheckKey statements also mark accounts as validated (PDA key implies type safety).

Pass 2 — Violation detection:

  • Scans all HirStmt::Assign with HirExpr::AccountData at offset > 0 for accounts not in the validated set.
  • Scans all HirStmt::StoreAccountData at 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.

References