Skip to main content
Sigvex

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:

  1. A lending protocol stores loan data in a PDA derived as: find_program_address(&[b"loan", borrower.key], program_id).
  2. The repay_loan instruction accepts a loan_account and checks loan_account.key == expected_loan.
  3. The check computes expected_loan using the provided borrower key — but does not verify the loan_account PDA was derived from the program’s own ID.
  4. An attacker creates an account at a PDA derived from a different program with the same seeds.
  5. The account content is crafted to look like a loan with a larger balance.
  6. The attacker repays less than they owe, as the fake PDA shows a different loan balance.

Authority bypass via PDA confusion:

  1. A program uses a PDA as the authority over a token account.
  2. The PDA is verified only by checking the key, not by calling find_program_address with the canonical seeds.
  3. An attacker constructs a fake PDA using bump seeds that produce a different address.
  4. The attacker passes this fake PDA as the authority.
  5. 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:

  1. The engine identifies the vulnerable instruction and the seeds expected by the program from the finding location.
  2. 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.
  3. An account is initialized at the fake PDA address with crafted data (e.g., inflated loan balance).
  4. The exploit transaction passes the fake PDA account to the victim program’s instruction.
  5. 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.
  6. 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

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_address at 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.

References