Skip to main content
Sigvex

PDA Validation Remediation

How to fix missing PDA address validation that allows attackers to substitute attacker-controlled accounts.

PDA Validation Remediation

Overview

Related Detector: PDA Validation

A PDA is only secure if the program checks that a passed-in account’s address matches the address derived from the expected seeds. Without this check, an attacker can pass a regular keypair account they control and bypass every assumption that depends on PDA ownership.

Before (Vulnerable)

pub fn withdraw(accounts: &[AccountInfo], user: Pubkey) -> ProgramResult {
    let vault = &accounts[0];

    // VULNERABLE: vault could be any account.
    let mut data = vault.try_borrow_mut_data()?;
    let amount = u64::from_le_bytes(data[..8].try_into().unwrap());
    transfer_lamports(vault, &accounts[1], amount)?;
    Ok(())
}

After (Fixed)

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

    // Re-derive the expected PDA and compare.
    let (expected, _bump) = Pubkey::find_program_address(
        &[b"vault", user.as_ref()],
        program_id,
    );
    if vault.key != &expected {
        return Err(ProgramError::InvalidSeeds);
    }

    // Also verify ownership — otherwise an attacker could pre-create
    // an account at the expected address with a different owner.
    if vault.owner != program_id {
        return Err(ProgramError::IllegalOwner);
    }

    let mut data = vault.try_borrow_mut_data()?;
    let amount = u64::from_le_bytes(data[..8].try_into().unwrap());
    transfer_lamports(vault, &accounts[1], amount)?;
    Ok(())
}

Address validation alone is insufficient — pair it with an owner check so an attacker cannot front-run by creating a system-owned account at the same address.

Alternative Mitigations

Store and verify the bump seed

let pda = Pubkey::create_program_address(
    &[b"vault", user.as_ref(), &[stored_bump]],
    program_id,
)?;
if vault.key != &pda {
    return Err(ProgramError::InvalidSeeds);
}

create_program_address is cheaper than find_program_address because it does not search for a bump. Store the canonical bump in account state at initialization and reuse it.

Use the account-validation framework’s seeds + bump constraint

#[account(
    mut,
    seeds = [b"vault", user.key().as_ref()],
    bump = vault.bump,
)]
pub vault: Account<'info, Vault>,

Anchor performs both the address derivation and the discriminator/owner check at deserialization. This is the lowest-effort safe pattern.

Common Mistakes

Verifying address but not owner. An attacker can call the System Program to allocate an account at any address, including a PDA address. Without an owner check, the program treats it as legitimate state.

Using find_program_address on every call. It costs ~1500 CU per invocation because it searches for the canonical bump. For hot paths, store the bump and use create_program_address.

Trusting client-supplied bumps. A non-canonical bump can derive a different valid PDA. Always store the canonical bump on initialization and reject any other value.

Skipping validation on read-only accounts. Even read-only PDAs control program logic (e.g., oracle PDAs). Validate them too.

References