Skip to main content
Sigvex

PDA Ownership Validation

Detects PDA usage without ownership validation, allowing unauthorized access.

PDA Ownership Validation

Overview

Remediation Guide: How to Fix PDA Ownership Validation

The PDA ownership validation detector identifies functions that operate on Program Derived Addresses without verifying that the PDA is owned by the expected program. Without ownership validation, an attacker can pass a malicious account with a PDA-like address owned by a different program, leading to unauthorized state access or manipulation.

Why This Is an Issue

A PDA address alone does not guarantee that the account was created by or belongs to your program. Any program can create an account at any address. If a program reads or writes PDA data without checking account.owner == program_id, an attacker can substitute an account at the same address but owned by a different program, potentially containing crafted data designed to bypass security checks.

CWE mapping: CWE-285 (Improper Authorization).

How to Resolve

Native Solana

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

    // 1. Derive expected PDA
    let (expected_pda, _bump) = Pubkey::find_program_address(&[b"vault"], program_id);

    // 2. Validate address
    if pda_account.key != &expected_pda {
        return Err(ProgramError::InvalidAccountData);
    }

    // 3. Validate owner
    if pda_account.owner != program_id {
        return Err(ProgramError::IllegalOwner);
    }

    // Safe to use PDA data
    Ok(())
}

Anchor

#[derive(Accounts)]
pub struct Process<'info> {
    // Account<'info, T> validates owner == program_id at deserialization
    #[account(seeds = [b"vault"], bump)]
    pub vault: Account<'info, Vault>,
}

Examples

Vulnerable Code

pub fn withdraw_pda(accounts: &[AccountInfo]) -> ProgramResult {
    let vault = &accounts[0]; // No owner check!
    let mut data = vault.data.borrow_mut();
    let balance = u64::from_le_bytes(data[0..8].try_into().unwrap());
    // Attacker's fake account could report any balance
    Ok(())
}

Fixed Code

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

    if vault.owner != program_id {
        return Err(ProgramError::IllegalOwner);
    }

    let data = vault.data.borrow();
    let balance = u64::from_le_bytes(data[0..8].try_into().unwrap());
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "pda-ownership-validation",
  "severity": "medium",
  "confidence": 0.65,
  "description": "Function 'withdraw_pda' appears to work with PDAs but contains no owner validation checks.",
  "location": { "function": "withdraw_pda" }
}

Detection Methodology

  1. PDA heuristics: Identifies functions with PDA-related operations (CPI calls, account data storage) or PDA-related names.
  2. Owner check scanning: Searches for CheckOwner statements in the function.
  3. Gap detection: Flags functions with PDA operations but no owner validation.
  4. Context adjustment: Confidence is significantly reduced for Anchor programs where Account<'info, T> validates ownership automatically.

Limitations

  • The detector uses naming heuristics (“pda”, “derived”, “seed”) which may miss PDA functions with unconventional names.
  • Owner checks in called functions or initialization code are not visible at the instruction level.
  • The detector may flag functions that only read PDA data without modification, where the impact is lower.

References