Skip to content

Commit 23c6ec5

Browse files
authored
Merge branch 'main' into fix/create-account-dos-prevention
2 parents b36a4cc + 9d68336 commit 23c6ec5

3 files changed

Lines changed: 221 additions & 13 deletions

File tree

program/src/processor/manage_authority.rs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -330,19 +330,27 @@ pub fn process_remove_authority(
330330
_ => return Err(AuthError::InvalidAuthenticationKind.into()),
331331
}
332332

333-
// Authorization
334-
if admin_header.role != 0 {
335-
let target_data = unsafe { target_auth_pda.borrow_data_unchecked() };
336-
// Safe copy target header
337-
let mut target_h_bytes = [0u8; std::mem::size_of::<AuthorityAccountHeader>()];
338-
target_h_bytes
339-
.copy_from_slice(&target_data[..std::mem::size_of::<AuthorityAccountHeader>()]);
340-
let target_header =
341-
unsafe { std::mem::transmute::<&[u8; 48], &AuthorityAccountHeader>(&target_h_bytes) };
342-
if target_header.discriminator != AccountDiscriminator::Authority as u8 {
343-
return Err(ProgramError::InvalidAccountData);
344-
}
333+
// Authorization - ALWAYS validate target authority
334+
let target_data = unsafe { target_auth_pda.borrow_data_unchecked() };
335+
// Safe copy target header
336+
let mut target_h_bytes = [0u8; std::mem::size_of::<AuthorityAccountHeader>()];
337+
target_h_bytes.copy_from_slice(&target_data[..std::mem::size_of::<AuthorityAccountHeader>()]);
338+
let target_header =
339+
unsafe { std::mem::transmute::<&[u8; 48], &AuthorityAccountHeader>(&target_h_bytes) };
340+
341+
// ALWAYS verify discriminator
342+
if target_header.discriminator != AccountDiscriminator::Authority as u8 {
343+
return Err(ProgramError::InvalidAccountData);
344+
}
345345

346+
// ALWAYS verify target belongs to THIS wallet (CRITICAL SECURITY CHECK)
347+
if target_header.wallet != *wallet_pda.key() {
348+
return Err(ProgramError::InvalidAccountData);
349+
}
350+
351+
// Role-based permission check
352+
if admin_header.role != 0 {
353+
// Admin can only remove Spender
346354
if admin_header.role != 1 || target_header.role != 2 {
347355
return Err(AuthError::PermissionDenied.into());
348356
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
use crate::common::{TestContext, ToAddress};
2+
use anyhow::Result;
3+
use solana_instruction::{AccountMeta, Instruction};
4+
use solana_keypair::Keypair;
5+
use solana_message::Message;
6+
use solana_pubkey::Pubkey;
7+
use solana_signer::Signer;
8+
use solana_system_program;
9+
use solana_transaction::Transaction;
10+
11+
pub fn run(ctx: &mut TestContext) -> Result<()> {
12+
println!("\n🔒 Running Cross-Wallet Attack Scenarios...");
13+
14+
// Setup: Create TWO wallets
15+
let user_seed_a = rand::random::<[u8; 32]>();
16+
let owner_a = Keypair::new();
17+
let (wallet_a, _) = Pubkey::find_program_address(&[b"wallet", &user_seed_a], &ctx.program_id);
18+
let (vault_a, _) =
19+
Pubkey::find_program_address(&[b"vault", wallet_a.as_ref()], &ctx.program_id);
20+
let (owner_a_auth, _) = Pubkey::find_program_address(
21+
&[
22+
b"authority",
23+
wallet_a.as_ref(),
24+
Signer::pubkey(&owner_a).as_ref(),
25+
],
26+
&ctx.program_id,
27+
);
28+
29+
let user_seed_b = rand::random::<[u8; 32]>();
30+
let owner_b = Keypair::new();
31+
let (wallet_b, _) = Pubkey::find_program_address(&[b"wallet", &user_seed_b], &ctx.program_id);
32+
let (vault_b, _) =
33+
Pubkey::find_program_address(&[b"vault", wallet_b.as_ref()], &ctx.program_id);
34+
let (owner_b_auth, _) = Pubkey::find_program_address(
35+
&[
36+
b"authority",
37+
wallet_b.as_ref(),
38+
Signer::pubkey(&owner_b).as_ref(),
39+
],
40+
&ctx.program_id,
41+
);
42+
43+
// Create Wallet A
44+
println!("\n[Setup] Creating Wallet A...");
45+
let mut data_a = vec![0];
46+
data_a.extend_from_slice(&user_seed_a);
47+
data_a.push(0);
48+
data_a.push(0);
49+
data_a.extend_from_slice(&[0; 6]);
50+
data_a.extend_from_slice(Signer::pubkey(&owner_a).as_ref());
51+
52+
let create_a_ix = Instruction {
53+
program_id: ctx.program_id.to_address(),
54+
accounts: vec![
55+
AccountMeta::new(Signer::pubkey(&ctx.payer).to_address(), true),
56+
AccountMeta::new(wallet_a.to_address(), false),
57+
AccountMeta::new(vault_a.to_address(), false),
58+
AccountMeta::new(owner_a_auth.to_address(), false),
59+
AccountMeta::new_readonly(solana_system_program::id().to_address(), false),
60+
],
61+
data: data_a,
62+
};
63+
64+
let message_a = Message::new(
65+
&[create_a_ix],
66+
Some(&Signer::pubkey(&ctx.payer).to_address()),
67+
);
68+
let mut create_a_tx = Transaction::new_unsigned(message_a);
69+
create_a_tx.sign(&[&ctx.payer, &owner_a], ctx.svm.latest_blockhash());
70+
71+
ctx.execute_tx(create_a_tx)?;
72+
73+
// Create Wallet B
74+
println!("[Setup] Creating Wallet B...");
75+
let mut data_b = vec![0];
76+
data_b.extend_from_slice(&user_seed_b);
77+
data_b.push(0);
78+
data_b.push(0);
79+
data_b.extend_from_slice(&[0; 6]);
80+
data_b.extend_from_slice(Signer::pubkey(&owner_b).as_ref());
81+
82+
let create_b_ix = Instruction {
83+
program_id: ctx.program_id.to_address(),
84+
accounts: vec![
85+
AccountMeta::new(Signer::pubkey(&ctx.payer).to_address(), true),
86+
AccountMeta::new(wallet_b.to_address(), false),
87+
AccountMeta::new(vault_b.to_address(), false),
88+
AccountMeta::new(owner_b_auth.to_address(), false),
89+
AccountMeta::new_readonly(solana_system_program::id().to_address(), false),
90+
],
91+
data: data_b,
92+
};
93+
94+
let message_b = Message::new(
95+
&[create_b_ix],
96+
Some(&Signer::pubkey(&ctx.payer).to_address()),
97+
);
98+
let mut create_b_tx = Transaction::new_unsigned(message_b);
99+
create_b_tx.sign(&[&ctx.payer, &owner_b], ctx.svm.latest_blockhash());
100+
101+
ctx.execute_tx(create_b_tx)?;
102+
103+
// Scenario 1: Owner A tries to Add Authority to Wallet B
104+
println!("\n[1/3] Testing Cross-Wallet Authority Addition...");
105+
let attacker_keypair = Keypair::new();
106+
let (attacker_auth_b, _) = Pubkey::find_program_address(
107+
&[
108+
b"authority",
109+
wallet_b.as_ref(),
110+
Signer::pubkey(&attacker_keypair).as_ref(),
111+
],
112+
&ctx.program_id,
113+
);
114+
115+
let mut add_cross_data = vec![1];
116+
add_cross_data.push(0);
117+
add_cross_data.push(1); // Admin
118+
add_cross_data.extend_from_slice(&[0; 6]);
119+
add_cross_data.extend_from_slice(Signer::pubkey(&attacker_keypair).as_ref());
120+
add_cross_data.extend_from_slice(Signer::pubkey(&attacker_keypair).as_ref());
121+
122+
let cross_add_ix = Instruction {
123+
program_id: ctx.program_id.to_address(),
124+
accounts: vec![
125+
AccountMeta::new(Signer::pubkey(&ctx.payer).to_address(), true),
126+
AccountMeta::new(wallet_b.to_address(), false), // Target: Wallet B
127+
AccountMeta::new(owner_a_auth.to_address(), false), // Auth: Owner A (WRONG WALLET)
128+
AccountMeta::new(attacker_auth_b.to_address(), false),
129+
AccountMeta::new_readonly(solana_system_program::id().to_address(), false),
130+
AccountMeta::new_readonly(Signer::pubkey(&owner_a).to_address(), true), // Owner A signing
131+
],
132+
data: vec![1, 1],
133+
};
134+
135+
let message_cross = Message::new(
136+
&[cross_add_ix],
137+
Some(&Signer::pubkey(&ctx.payer).to_address()),
138+
);
139+
let mut cross_add_tx = Transaction::new_unsigned(message_cross);
140+
cross_add_tx.sign(&[&ctx.payer, &owner_a], ctx.svm.latest_blockhash());
141+
142+
ctx.execute_tx_expect_error(cross_add_tx)?;
143+
println!("✅ Cross-Wallet Authority Addition Rejected.");
144+
145+
// Scenario 2: Owner A tries to Remove Owner from Wallet B
146+
println!("\n[2/3] Testing Cross-Wallet Authority Removal...");
147+
let remove_cross_ix = Instruction {
148+
program_id: ctx.program_id.to_address(),
149+
accounts: vec![
150+
AccountMeta::new(Signer::pubkey(&ctx.payer).to_address(), true),
151+
AccountMeta::new(wallet_b.to_address(), false), // Target: Wallet B
152+
AccountMeta::new(owner_a_auth.to_address(), false), // Auth: Owner A (WRONG)
153+
AccountMeta::new(owner_b_auth.to_address(), false), // Target: Owner B
154+
AccountMeta::new(Signer::pubkey(&ctx.payer).to_address(), false),
155+
AccountMeta::new_readonly(Signer::pubkey(&owner_a).to_address(), true),
156+
],
157+
data: vec![2],
158+
};
159+
160+
let message_remove = Message::new(
161+
&[remove_cross_ix],
162+
Some(&Signer::pubkey(&ctx.payer).to_address()),
163+
);
164+
let mut remove_cross_tx = Transaction::new_unsigned(message_remove);
165+
remove_cross_tx.sign(&[&ctx.payer, &owner_a], ctx.svm.latest_blockhash());
166+
167+
ctx.execute_tx_expect_error(remove_cross_tx)?;
168+
println!("✅ Cross-Wallet Authority Removal Rejected.");
169+
170+
// Scenario 3: Owner A tries to Execute on Wallet B's Vault
171+
println!("\n[3/3] Testing Cross-Wallet Execution...");
172+
let mut exec_cross_data = vec![4];
173+
exec_cross_data.push(0); // Empty compact
174+
175+
let exec_cross_ix = Instruction {
176+
program_id: ctx.program_id.to_address(),
177+
accounts: vec![
178+
AccountMeta::new(Signer::pubkey(&ctx.payer).to_address(), true),
179+
AccountMeta::new(wallet_b.to_address(), false), // Target: Wallet B
180+
AccountMeta::new(owner_a_auth.to_address(), false), // Auth: Owner A (WRONG)
181+
AccountMeta::new(vault_b.to_address(), false), // Vault B
182+
AccountMeta::new_readonly(solana_system_program::id().to_address(), false),
183+
AccountMeta::new_readonly(Signer::pubkey(&owner_a).to_address(), true),
184+
],
185+
data: exec_cross_data,
186+
};
187+
188+
let message_exec = Message::new(
189+
&[exec_cross_ix],
190+
Some(&Signer::pubkey(&ctx.payer).to_address()),
191+
);
192+
let mut exec_cross_tx = Transaction::new_unsigned(message_exec);
193+
exec_cross_tx.sign(&[&ctx.payer, &owner_a], ctx.svm.latest_blockhash());
194+
195+
ctx.execute_tx_expect_error(exec_cross_tx)?;
196+
println!("✅ Cross-Wallet Execution Rejected.");
197+
198+
println!("\n✅ All Cross-Wallet Attack Scenarios Passed!");
199+
Ok(())
200+
}

tests-e2e/src/scenarios/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
pub mod dos_attack;
1+
pub mod cross_wallet_attacks;
22
pub mod failures;
33
pub mod happy_path;

0 commit comments

Comments
 (0)