PDA Manipulation Exploit Generator
Sigvex exploit generator that validates PDA (Program Derived Address) manipulation vulnerabilities in Solana programs by simulating an attack that passes a fake PDA to bypass program authority validation.
PDA Manipulation Exploit Generator
Overview
The PDA manipulation exploit generator validates findings from the pda-validation detector by simulating an attack that derives a fake PDA and passes it to an instruction that does not verify the PDA’s derivation path or the expected seeds. If the program accepts the fake PDA without verifying it was derived from the expected program and seeds, the vulnerability is confirmed as likely exploitable with confidence 0.80.
Program Derived Addresses (PDAs) are Solana’s mechanism for programs to control accounts — a PDA is an address derived from a program ID and seeds, with no corresponding private key. Programs use PDAs to hold assets they exclusively control. A vulnerability arises when a program accepts a PDA-shaped address without verifying it was derived from the correct program and seeds. An attacker can derive a PDA from different seeds (or a different program ID) that passes a simple key equality check but represents an account the attacker controls.
Severity score: 85/100 (High).
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
Fake PDA substitution:
- A lending protocol stores loan data in a PDA derived as:
find_program_address(&[b"loan", borrower.key], program_id). - The
repay_loaninstruction accepts aloan_accountand checksloan_account.key == expected_loan. - The check computes
expected_loanusing the providedborrowerkey — but does not verify theloan_accountPDA was derived from the program’s own ID. - An attacker creates an account at a PDA derived from a different program with the same seeds.
- The account content is crafted to look like a loan with a larger balance.
- The attacker repays less than they owe, as the fake PDA shows a different loan balance.
Authority bypass via PDA confusion:
- A program uses a PDA as the authority over a token account.
- The PDA is verified only by checking the key, not by calling
find_program_addresswith the canonical seeds. - An attacker constructs a fake PDA using bump seeds that produce a different address.
- The attacker passes this fake PDA as the authority.
- If the program uses the provided PDA for a CPI call, the fake PDA has signer privileges over accounts it controls — not the legitimate PDA’s accounts.
Exploit Mechanics
The engine maps pda-validation detector findings to the PDA manipulation exploit pattern (severity score 85/100).
Strategy: Derive a fake PDA using the same seeds but a different program ID, or using different seeds entirely, and pass it to an instruction that checks only key equality rather than re-deriving via find_program_address.
Simulation steps:
- The engine identifies the vulnerable instruction and the seeds expected by the program from the finding location.
- A fake PDA is computed:
Pubkey::find_program_address(&[b"loan", borrower_key.as_ref()], attacker_program_id). This produces a different address than the legitimate PDA (..., victim_program_id), but both are valid-looking PDAs. - An account is initialized at the fake PDA address with crafted data (e.g., inflated loan balance).
- The exploit transaction passes the fake PDA account to the victim program’s instruction.
- If the program accepts the account without verifying it was derived from the correct program ID and expected seeds, the vulnerability is confirmed as likely exploitable with confidence 0.80.
- The exploit result records:
status = Likely,description = "PDA authority bypass",impact = "High".
Why key equality is insufficient:
// VULNERABLE: Key equality check does not verify which program derived the PDA
require!(
ctx.accounts.loan_account.key() == derive_expected(&borrower),
LendingError::InvalidLoan
);
// An attacker can pass an account at a key derived from:
// Pubkey::find_program_address(&same_seeds, &attacker_controlled_program)
// which satisfies the equality check but is not controlled by the victim program
// VULNERABLE: Accepts any account — no PDA verification
use anchor_lang::prelude::*;
#[program]
mod vulnerable_lending {
pub fn close_loan(ctx: Context<CloseLoan>) -> Result<()> {
// VULNERABLE: Only checks the key matches, not the derivation path
require!(
ctx.accounts.loan.key() == ctx.accounts.expected_loan.key(),
LendingError::InvalidLoan
);
// ... close loan logic
Ok(())
}
}
// ATTACK: Derive a fake loan PDA from a different program
// Seeds are the same, but the program_id in the derivation differs
// The attacker creates an account matching the derived key with crafted data
// SECURE: Use Anchor's seeds and bump constraints
#[derive(Accounts)]
pub struct CloseLoanSafe<'info> {
#[account(
mut,
seeds = [b"loan", borrower.key().as_ref()],
bump = loan.bump, // Verified canonical bump
has_one = borrower,
close = borrower // Returns rent to borrower
)]
pub loan: Account<'info, Loan>,
pub borrower: Signer<'info>,
}
// Without Anchor: explicitly verify PDA derivation
fn verify_pda(
account: &AccountInfo,
seeds: &[&[u8]],
program_id: &Pubkey,
) -> Result<u8, ProgramError> {
let (expected_pda, bump) = Pubkey::find_program_address(seeds, program_id);
if account.key != &expected_pda {
return Err(ProgramError::InvalidArgument);
}
Ok(bump)
}
Remediation
- Detector: PDA Manipulation Detector
- Remediation Guide: PDA Manipulation Remediation
Always verify PDA derivation explicitly, never just key equality:
// Anchor: seeds + bump constraint verifies the complete derivation path
#[account(
seeds = [b"vault", authority.key().as_ref()],
bump, // Anchor stores and verifies the canonical bump
)]
pub vault: Account<'info, Vault>,
// Native: call find_program_address and compare
let (expected_pda, _bump) = Pubkey::find_program_address(
&[b"vault", authority.key.as_ref()],
program_id,
);
if vault_account.key != &expected_pda {
return Err(ProgramError::InvalidArgument);
}
Additional rules:
- Always store the canonical bump seed in the PDA account data during initialization.
- Use the stored bump (not
find_program_addressat runtime) for CPI signing to avoid manipulation. - Never derive a PDA without specifying the exact program ID — a PDA derived from a different program is not controlled by your program.