Skip to main content
Sigvex

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

  1. User input identification: Tracks variables assigned from parameters, account data reads, and memory loads as user-controlled.
  2. Validation detection: Records variables that appear in comparison operations or CheckKey statements as validated.
  3. PDA seed scanning: Checks find_program_address, create_program_address, and invoke_signed seeds for unvalidated user-controlled variables.
  4. 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).

References