RustFund

First Flight #36
Beginner FriendlyRust
100 EXP
View results
Submission Details
Severity: high
Invalid

Improper Ownership and PDA Validation Enables Unauthorized Fund Redirection

Summary

The FundContribute struct lacks necessary ownership and PDA validation, allowing attackers to substitute arbitrary accounts. Since Solana’s Anchor framework relies on account constraints to enforce security, missing constraints create a critical vulnerability, enabling fund redirection.

Vulnerability Details

  1. Ownership Check Missing:

    • The fund account does not enforce that it is owned by the program (has_one = creator), which allows an attacker to substitute any arbitrary account in place of the intended fund.

    • This opens the door for unauthorized access and account substitution attacks , where an attacker could redirect contributions to a malicious fund.

  2. PDA Validation Missing:

    • The fund account does not validate its derivation as a PDA using the appropriate seeds (e.g., [b"fund", fund.creator.as_ref()]).

    • Without PDA validation, an attacker could pass an arbitrary account that satisfies the Account<'info, Fund> type but is not the correct PDA-derived fund.

Impact

  1. Unauthorized Fund Manipulation:
    Attackers can pass arbitrary accounts for fund, bypassing crowdfunding-specific rules.

  2. Account Takeover:
    Attackers can manipulate contributions of other users by passing incorrect contribution accounts.

  3. Trust & Security Violation:
    Contributors may lose funds due to malicious manipulation of accounts, undermining the platform’s guarantees of transparency and security.

Proof of Concept

The following PoC demonstrates how an attacker can substitute an unauthorized fund PDA and redirect contributions. By using Pubkey::find_program_address with an arbitrary seed, the attacker generates a fake PDA and bypasses validation.

// Attacker generates a fake fund PDA instead of the expected one
let fake_fund_pda = Pubkey::find_program_address(&[b"fake_seed"], ctx.program_id);
// Attacker submits a contribution but routes funds to a fake campaign
let tx = program.methods
.contribute(amount)
.accounts({
fund: fake_fund_pda, // Bypasses the expected fund validation
contributor: attacker.key(),
contribution: attacker_contribution_account, // Fake contribution record
system_program: system_program::ID,
})
.signers([attacker])
.rpc();

Recommendations

Option One: Anchor Constraints

Update the FundContribute struct as follows:

#[derive(Accounts)]
pub struct FundContribute<'info> {
#[account(
mut,
seeds = [b"fund", fund.creator.as_ref()],
bump,
has_one = creator // Ensure fund is owned by the creator
)]
pub fund: Account<'info, Fund>,
#[account(mut)]
pub contributor: Signer<'info>,
#[account(
init_if_needed,
payer = contributor,
space = 8 + Contribution::INIT_SPACE,
seeds = [fund.key().as_ref(), contributor.key().as_ref()],
bump,
has_one = contributor, // Ensure contributor owns this record
has_one = fund // Ensure contribution is linked to the correct fund
)]
pub contribution: Account<'info, Contribution>,
pub system_program: Program<'info, System>,
}

Option Two: Explicit Checks in Function Body:

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
let fund = &ctx.accounts.fund;
let contribution = &ctx.accounts.contribution;
let contributor = &ctx.accounts.contributor;
// Validate fund PDA
let (expected_fund_pda, _) = Pubkey::find_program_address(
&[b"fund", fund.creator.as_ref()],
ctx.program_id,
);
if fund.key() != expected_fund_pda {
return Err(ErrorCode::UnauthorizedAccess.into());
}
// Validate contribution ownership
if contribution.contributor != contributor.key() {
return Err(ErrorCode::UnauthorizedAccess.into());
}
// Rest of the function logic...
}
Updates

Lead Judging Commences

bube Lead Judge
3 months ago

Appeal created

bube Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
SashaFlores Submitter
3 months ago
bube Lead Judge
3 months ago
bube Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.