Skip to main content
Sigvex

PDA Signer Seed Extraction

Detects PDA signer seeds that can be extracted or predicted by attackers, enabling PDA authority bypass.

PDA Signer Seed Extraction

Overview

Remediation Guide: How to Fix PDA Signer Seed Extraction

The PDA signer seed extraction detector identifies Solana programs where PDA signer seeds can be reconstructed or predicted by attackers. When a program uses invoke_signed with seeds derived from user-controlled input, or exposes seed components through logs or return data, an attacker can derive the same PDA and forge CPI signatures. This bypasses all authority checks that rely on PDA ownership.

The detector tracks three vulnerable patterns:

  1. User-controlled seed components: CPI signer seeds derived from instruction data or caller-supplied accounts without domain separation.
  2. Seed exposure via logs: Seed values or seed-derived variables written to program logs.
  3. Predictable seed components: Seeds using sequential IDs, timestamps, or other values an attacker can enumerate.

Why This Is an Issue

PDAs (Program Derived Addresses) serve as program-owned authorities on Solana. When a program calls invoke_signed, it provides the seeds that derive the PDA, proving it has authority over that address. If an attacker can reconstruct these seeds:

  • The attacker calls the same program with the reconstructed seeds
  • invoke_signed succeeds because the seeds correctly derive the PDA
  • The attacker now has the same authority as the PDA
  • Any tokens, accounts, or state controlled by the PDA can be taken
  • The attacker can execute arbitrary operations on behalf of the PDA

This is particularly dangerous in programs where the PDA controls token vaults, governance authority, or upgrade keys.

CWE mapping: CWE-330 (Use of Insufficiently Random Values), CWE-522 (Insufficiently Protected Credentials).

How to Resolve

// Before: Vulnerable -- seeds derived from user input without domain separation
pub fn transfer_from_vault(
    accounts: &[AccountInfo],
    user_seed: &[u8],
    amount: u64,
) -> ProgramResult {
    let vault = &accounts[0];
    let destination = &accounts[1];

    // VULNERABLE: attacker controls user_seed
    let seeds = &[b"vault", user_seed];
    let (pda, bump) = Pubkey::find_program_address(seeds, &program_id);

    let signer_seeds = &[b"vault".as_ref(), user_seed, &[bump]];
    let ix = system_instruction::transfer(&pda, destination.key, amount);
    invoke_signed(&ix, accounts, &[signer_seeds])?;
    Ok(())
}

// After: Use program-controlled seeds with domain separation
pub fn transfer_from_vault(
    accounts: &[AccountInfo],
    amount: u64,
    program_id: &Pubkey,
) -> ProgramResult {
    let vault = &accounts[0];
    let destination = &accounts[1];
    let authority = &accounts[2];

    // FIXED: verify authority
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // FIXED: seeds include program_id for domain separation
    // and use authority.key (verified signer) not raw user input
    let seeds = &[b"vault", authority.key.as_ref()];
    let (pda, bump) = Pubkey::find_program_address(seeds, program_id);

    if *vault.key != pda {
        return Err(ProgramError::InvalidSeeds);
    }

    let signer_seeds = &[b"vault".as_ref(), authority.key.as_ref(), &[bump]];
    let ix = system_instruction::transfer(&pda, destination.key, amount);
    invoke_signed(&ix, accounts, &[signer_seeds])?;
    Ok(())
}

Examples

Vulnerable Code

pub fn claim_reward(
    accounts: &[AccountInfo],
    reward_id: u64,  // User-supplied, predictable
    program_id: &Pubkey,
) -> ProgramResult {
    let reward_vault = &accounts[0];
    let claimer = &accounts[1];

    // Seeds are fully predictable from public data
    let seeds = &[b"reward", &reward_id.to_le_bytes()];
    let (pda, bump) = Pubkey::find_program_address(seeds, program_id);

    // Attacker enumerates reward_id values to derive all PDAs
    let signer_seeds = &[b"reward".as_ref(), &reward_id.to_le_bytes(), &[bump]];
    let ix = system_instruction::transfer(&pda, claimer.key, reward_amount);
    invoke_signed(&ix, accounts, &[signer_seeds])?;
    Ok(())
}

Fixed Code

pub fn claim_reward(
    accounts: &[AccountInfo],
    program_id: &Pubkey,
) -> ProgramResult {
    let reward_vault = &accounts[0];
    let claimer = &accounts[1];
    let reward_record = &accounts[2];

    // FIXED: verify claimer is signer
    if !claimer.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // FIXED: seeds include claimer key (verified signer)
    let seeds = &[b"reward", claimer.key.as_ref()];
    let (pda, bump) = Pubkey::find_program_address(seeds, program_id);

    if *reward_vault.key != pda {
        return Err(ProgramError::InvalidSeeds);
    }

    // FIXED: verify reward hasn't been claimed
    let record_data = reward_record.try_borrow_data()?;
    let record = RewardRecord::try_from_slice(&record_data[8..])?;
    if record.claimed {
        return Err(ProgramError::InvalidAccountData);
    }

    let signer_seeds = &[b"reward".as_ref(), claimer.key.as_ref(), &[bump]];
    let ix = system_instruction::transfer(&pda, claimer.key, record.amount);
    invoke_signed(&ix, accounts, &[signer_seeds])?;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "pda-signer-seed-extraction",
  "severity": "critical",
  "confidence": 0.82,
  "description": "Function 'claim_reward' performs a CPI with signer seeds that are derived from user-controlled input without sufficient domain separation. An attacker may reconstruct the seeds and forge PDA signatures.",
  "location": { "function": "claim_reward", "offset": 4 }
}

Detection Methodology

The detector performs taint analysis on seed variables:

  1. User input collection: Identifies variables sourced from instruction data, account keys, or function parameters.
  2. Seed taint propagation: Tracks how user-input variables flow through assignments into seed expressions used in invoke_signed calls.
  3. CPI seed analysis: When an InvokeCpi with seeds is found, checks whether any seed component is tainted by user input.
  4. Log exposure detection: Checks whether seed-derived variables are passed to Log statements.
  5. Domain separation check: Evaluates whether seeds include program-controlled components (hardcoded strings, program ID) that provide domain separation. Pure user-input seeds receive higher severity.
  6. PDA cross-referencing: When PDA-derived account information is available from earlier analysis, cross-references seed extraction findings with known PDA authorities.

Limitations

False positives:

  • Programs where user-controlled seed components are intentionally used (e.g., user-specific PDAs where the user’s key is a seed) and access control is enforced by signer checks.
  • Programs with multi-step seed derivation where intermediate validation ensures seed integrity.

False negatives:

  • Seeds derived from account data that was itself derived from user input in a prior instruction.
  • Complex seed construction through multiple helper functions.

References