Accounts array is as reliable as you make it!
Note: Even though the user sends the accounts array, it is the program which decides the sequence of the accounts and communicates that to the user. The user is then required to provide the necessary accounts in the mentioned sequence to make the transaction work.
But you are right, the user can decide to ignore the sequence and pretty much send any accounts in the transaction. That's where proper validation comes into picture.
Consider the following function:
pub fn process_my_ix(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let first_acc_info = next_account_info(account_info_iter)?; // 0
....
As you can see, first_acc_info
is the first account we expect the user to provide us. The user can provide us any account but we want the first account to be the signer of the transaction. So, we will step forward and validate this account within the function:
...
if !first_acc_info.is_signer {
msg!("Missing required signature");
return Err(ProgramError::MissingRequiredSignature)
}
...
Now, it isn't possible for the user to enter a non-signer account as the first account in the array. This is how you must validate every account according to the logic of the program to avoid any kind of malicious activity from the user. A proper validation is the basic pillar of a program (smart contract).
PS: Anchor makes such checks much easier.