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
Deposit instruction does not validate extensions PDA, allowing hook bypass
Summary
The
Depositinstruction 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, causingget_extensions_from_accountto return no hooks, silently skipping all pre- and post-deposit hook invocations.The
Withdrawinstruction correctly validates the extensions PDA at processor.rs line 39 viavalidate_extensions_pda(). TheAllowMint,BlockTokenExtension, andSetHookinstructions also validate it. OnlyDepositis missing this check.Details
In
deposit/accounts.rslines 73-86, the account validation performs:In
deposit/processor.rsline 74, the extensions account is read directly:The function
get_extensions_from_account(escrow_extensions.rs line 321) checksdata_len()first -- if zero, it returnsOk(vec![None]). No hooks are found, so the hook invocations at lines 78 and 101 are skipped viaif let Some(ref hook) = hook_data.Compare with
withdraw/processor.rsline 39: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