Skip to content

Deposit instruction missing extensions PDA validation allows hook bypass

Moderate
dev-jodee published GHSA-735q-4mm8-3j4w Feb 20, 2026

Software

spl-escrow

Affected versions

<= 0.1.0

Patched versions

None

Description

Deposit instruction does not validate extensions PDA, allowing hook bypass

Summary

The Deposit instruction reads hook configuration from the extensions account but does not validate that it is the correct PDA. An attacker can pass any empty account (e.g., the system program) as the extensions parameter, causing get_extensions_from_account to return no hooks, silently skipping all pre- and post-deposit hook invocations.

The Withdraw instruction correctly validates the extensions PDA at processor.rs line 39 via validate_extensions_pda(). The AllowMint, BlockTokenExtension, and SetHook instructions also validate it. Only Deposit is missing this check.

Details

In deposit/accounts.rs lines 73-86, the account validation performs:

verify_readonly(extensions)?;              // line 76 -- only checks readonly flag
// ...
verify_current_program_account(escrow)?;   // line 85 -- ownership check
verify_current_program_account(allowed_mint)?; // line 86 -- ownership check
// extensions: no ownership or PDA check

In deposit/processor.rs line 74, the extensions account is read directly:

let exts = get_extensions_from_account(ix.accounts.extensions, &[ExtensionType::Hook])?;

The function get_extensions_from_account (escrow_extensions.rs line 321) checks data_len() first -- if zero, it returns Ok(vec![None]). No hooks are found, so the hook invocations at lines 78 and 101 are skipped via if let Some(ref hook) = hook_data.

Compare with withdraw/processor.rs line 39:

validate_extensions_pda(ix.accounts.escrow, ix.accounts.extensions, program_id)?;

This checks both PDA derivation (seeds ["extensions", escrow_key]) and the stored bump byte.

Anticipated counterargument

"Hooks are an optional security feature, and the escrow admin opted into them -- it's the integrator's responsibility to ensure correct accounts are passed." -- The escrow program provides hooks specifically to enforce deposit policies (KYC, whitelists, rate limits). The withdraw path validates the extensions PDA precisely because hook enforcement should not be bypassable by the caller. Leaving the deposit path unvalidated means any user can construct a transaction that skips all configured deposit hooks, regardless of the admin's intent.

Impact

Any pre-deposit or post-deposit hook can be bypassed by passing an empty system-owned account as the extensions parameter. If an escrow admin configured a hook to enforce deposit restrictions (allowlists, amount limits, compliance checks), those restrictions are ineffective. The deposit succeeds normally, creating a valid receipt that can later be withdrawn through the correctly-validated withdraw path.

Fix

Add the same validation that withdraw uses, before reading extensions:

+ validate_extensions_pda(ix.accounts.escrow, ix.accounts.extensions, program_id)?;
  let exts = get_extensions_from_account(ix.accounts.extensions, &[ExtensionType::Hook])?;

References

Severity

Moderate

CVE ID

No known CVE

Weaknesses

No CWEs

Credits