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.
Recommended Fix
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.