PDA User-Controlled Seeds
Detects user-controlled input used in PDA seeds without validation, enabling arbitrary PDA derivation.
PDA User-Controlled Seeds
Overview
Remediation Guide: How to Fix PDA User-Controlled Seeds
The PDA user-controlled seeds detector identifies cases where user-provided data (instruction parameters, account data) flows into PDA derivation without validation. An attacker can provide arbitrary seed values to derive any PDA address, potentially bypassing access controls or manipulating accounts they should not have access to.
Why This Is an Issue
PDA seeds determine which account is accessed. If seeds come entirely from user input without validation, the user controls which account the program operates on. This can bypass access control checks that depend on the PDA pointing to the correct account, enable access to other users’ accounts by supplying their identifiers as seeds, or create PDAs that collide with existing accounts. While using user public keys as seeds is a common legitimate pattern, raw unvalidated instruction data as seeds is dangerous.
CWE mapping: CWE-20 (Improper Input Validation).
How to Resolve
Native Solana
// VULNERABLE: raw instruction data as seed
let user_seed = &instruction_data[0..32]; // No validation!
let (pda, _) = find_program_address(&[user_seed], program_id);
// FIXED: validate input before use in seeds
if user_seed.len() > MAX_SEED_LEN {
return Err(ProgramError::InvalidArgument);
}
let (pda, _) = find_program_address(&[b"prefix", user_seed], program_id);
Anchor
#[derive(Accounts)]
pub struct Process<'info> {
// seeds defined declaratively -- user cannot inject arbitrary seeds
#[account(seeds = [b"vault", user.key().as_ref()], bump)]
pub vault: Account<'info, Vault>,
pub user: Signer<'info>,
}
Examples
Vulnerable Code
pub fn access_account(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
// User controls which PDA is derived
let (pda, _) = Pubkey::find_program_address(&[data], program_id);
let account = find_account(accounts, &pda)?;
let mut account_data = account.data.borrow_mut();
account_data[0] = 1; // Writing to user-chosen account!
Ok(())
}
Fixed Code
pub fn access_account(accounts: &[AccountInfo], user: &Pubkey) -> ProgramResult {
// Validate the user is the signer
let user_account = &accounts[0];
if !user_account.is_signer || user_account.key != user {
return Err(ProgramError::MissingRequiredSignature);
}
// Use validated signer key as seed
let (pda, _) = Pubkey::find_program_address(
&[b"user-data", user.as_ref()],
program_id,
);
Ok(())
}
Sample Sigvex Output
{
"detector_id": "pda-user-controlled-seeds",
"severity": "high",
"confidence": 0.78,
"description": "PDA derivation in find_program_address uses user-controlled input (param0) without validation.",
"location": { "function": "access_account", "block": 0, "stmt": 0 }
}
Detection Methodology
- User input identification: Tracks variables assigned from parameters, account data reads, and memory loads as user-controlled.
- Validation detection: Records variables that appear in comparison operations or
CheckKeystatements as validated. - PDA seed scanning: Checks
find_program_address,create_program_address, andinvoke_signedseeds for unvalidated user-controlled variables. - Taint propagation: Follows variable assignments to detect indirect user input flowing into seeds.
Limitations
- Using signer public keys as seeds is a common and safe pattern, but the detector may flag it if the signer check is not visible in the same function.
- Validation through helper functions or separate instructions is not tracked.
- The detector cannot distinguish between intentionally user-controlled seeds (legitimate) and accidentally user-controlled seeds (vulnerability).
Related Detectors
- PDA Seed Validation — validates overall PDA seed patterns
- PDA Seed Collision — detects seed collision vulnerabilities
- Input Validation — general input validation checks