Skip to main content
Sigvex

PDA Signer Seed Extraction Remediation

How to protect PDA signer seeds from extraction by using domain separation and program-controlled seed components.

PDA Signer Seed Extraction Remediation

Overview

Related Detector: PDA Signer Seed Extraction

PDA signer seed extraction occurs when seed components used in invoke_signed can be predicted or reconstructed by an attacker, allowing them to derive the same PDA and forge CPI signatures. The fix is to ensure seeds include program-controlled components and that access control does not rely solely on seed knowledge.

Before (Vulnerable)

pub fn process(accounts: &[AccountInfo], user_data: &[u8]) -> ProgramResult {
    // Seeds derived entirely from user input
    let seeds = &[b"auth", user_data];
    let (pda, bump) = Pubkey::find_program_address(seeds, program_id);
    let signer_seeds = &[b"auth".as_ref(), user_data, &[bump]];

    invoke_signed(&transfer_ix, accounts, &[signer_seeds])?;
    Ok(())
}

After (Fixed)

pub fn process(accounts: &[AccountInfo], program_id: &Pubkey) -> ProgramResult {
    let authority = &accounts[0];

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

    // FIXED: seeds use verified signer key, not raw user data
    let seeds = &[b"auth", authority.key.as_ref()];
    let (pda, bump) = Pubkey::find_program_address(seeds, program_id);

    // FIXED: verify PDA matches expected account
    if *accounts[1].key != pda {
        return Err(ProgramError::InvalidSeeds);
    }

    let signer_seeds = &[b"auth".as_ref(), authority.key.as_ref(), &[bump]];
    invoke_signed(&transfer_ix, accounts, &[signer_seeds])?;
    Ok(())
}

Alternative Mitigations

1. Anchor PDA constraints

Anchor validates PDA derivation automatically and stores the bump:

#[derive(Accounts)]
pub struct TransferFromVault<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(
        mut,
        seeds = [b"vault", authority.key().as_ref()],
        bump = vault.bump,
    )]
    pub vault: Account<'info, VaultState>,
}

pub fn transfer_from_vault(ctx: Context<TransferFromVault>, amount: u64) -> Result<()> {
    // PDA derivation is validated by Anchor
    // Seeds are tied to the verified signer
    Ok(())
}

2. Multi-component seeds with program-controlled elements

Add a program-controlled nonce or state-derived component:

let seeds = &[
    b"vault",
    authority.key.as_ref(),
    &vault_state.nonce.to_le_bytes(),  // Program-controlled counter
];

The nonce is stored in program state and is not user-controllable.

3. Do not log seed components

// WRONG: exposes seed material
msg!("Processing vault with seed: {:?}", seed_bytes);

// CORRECT: log only non-sensitive identifiers
msg!("Processing vault: {}", vault_pda);

Common Mistakes

Mistake 1: Using sequential IDs as the sole non-static seed

// WRONG: reward_id is enumerable
let seeds = &[b"reward", &reward_id.to_le_bytes()];

An attacker can iterate over all possible reward_id values to derive every PDA.

Mistake 2: Relying on seed secrecy for security

// WRONG: "secret" seed in source code is not secret
let seeds = &[b"secret_seed_do_not_share", user.key.as_ref()];

All deployed program bytecode is publicly readable. Seeds derived from constants in the binary are not secret. Use access control (signer checks, owner checks) instead of seed secrecy.

Mistake 3: Exposing seeds through return data

// WRONG: seeds sent back to caller
sol_set_return_data(&seed_bytes);

Return data is visible to all programs in the CPI chain and to the transaction submitter.

References