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
- PDA heuristics: Identifies functions with PDA-related operations (CPI calls, account data storage) or PDA-related names.
- Owner check scanning: Searches for
CheckOwnerstatements in the function. - Gap detection: Flags functions with PDA operations but no owner validation.
- 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.
Related Detectors
- PDA Validation — validates PDA address derivation
- Missing Owner Check — general account ownership validation