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:
- User-controlled seed components: CPI signer seeds derived from instruction data or caller-supplied accounts without domain separation.
- Seed exposure via logs: Seed values or seed-derived variables written to program logs.
- 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_signedsucceeds 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:
- User input collection: Identifies variables sourced from instruction data, account keys, or function parameters.
- Seed taint propagation: Tracks how user-input variables flow through assignments into seed expressions used in
invoke_signedcalls. - CPI seed analysis: When an
InvokeCpiwith seeds is found, checks whether any seed component is tainted by user input. - Log exposure detection: Checks whether seed-derived variables are passed to
Logstatements. - 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.
- Blackboard integration: When PDA-derived account information is available from the blackboard, 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.
Related Detectors
- PDA Validation — missing PDA address verification
- PDA Seed Collision — seed combinations that can produce the same PDA
- Bump Seed Canonicalization — non-canonical bump seed usage