Create your own
Lesson illustration

Validating Account Ownership and Signer Privileges

Welcome back! In our previous lesson, we dove into the AccountInfo struct and learned how to read and deserialize the data stored within on-chain accounts. You now know how to access the state your program needs to operate on.

However, just because you can read an account's data doesn't mean you should trust it or have permission to change it. On Solana, the client that builds a transaction specifies all the accounts the program will interact with. This means a malicious actor could try to pass in incorrect or fake accounts to trick your program.

This brings us to today's crucial topic: security. Our learning outcome is to manually validate account ownership and signer privileges in program logic. We'll move from simply reading AccountInfo to scrutinizing it, ensuring that our program only acts on the right accounts with the right permissions. This is a non-negotiable skill for writing safe and secure Solana programs.

The Attacker-Controlled Model

The fundamental security principle on Solana is that your program should trust nothing it is given. The list of accounts and the instruction data are all provided by the client, which could be an attacker. This is often called the "attacker-controlled" model. Your program's first job is to be paranoid and validate every input before proceeding.

A Hitchhiker's Guide to Solana Program Security

The article "A Hitchhiker's Guide to Solana Program Security" from Helius provides an excellent explanation of this core concept. Understanding this mindset is the first step to writing secure programs.

Read the sections titled "Solana’s Programming Model" and "Solana is Attacker-Controlled". Focus on the key takeaway: because anyone can pass any account to your program, data validation is not optional—it's the foundation of security.

This principle means we need to perform explicit checks. The two most fundamental checks are for ownership and signer privileges. Let's examine each.

1. Validating Account Ownership

In the last lesson, you saw that every data account has an owner field. This field contains the public key of the program that has permission to modify the account's data. When your program receives an account that it's supposed to write to (e.g., a counter account it needs to increment), it must verify that it is, in fact, the owner of that account.

If you skip this check, an attacker could create their own account, fill it with malicious data, and pass it to your program. If your program blindly writes to it, at best it fails; at worst, it could lead to exploits if other parts of the system read that malicious data.

How to Check Ownership

The check itself is a simple comparison within your program's logic. You compare the owner public key from the AccountInfo struct with your own program's ID, which is passed into the entrypoint.

// Inside your process_instruction function...
let accounts_iter = &mut accounts.iter();
let data_account = next_account_info(accounts_iter)?;

// The crucial ownership check
if *data_account.owner != program_id {
    msg!("Error: The account is not owned by this program.");
    return Err(ProgramError::IncorrectProgramId);
}

The program_id is the public key of the currently executing program. This check ensures your program isn't being tricked into modifying an account that belongs to another program.

Building SmartContracts With Solana and Rust Lang

To see this check in the context of a program, watch this segment from David Choi's video. He explains why the check is necessary right before the program attempts to deserialize and modify account data.

Watch the clip titled "Validating Account Ownership in Program Logic" (43:56 - 45:26). Notice how the check confirming that the owner of this account... is this program is the first thing that happens after getting the account from the iterator.

What Happens When You Forget?

Let's look at a concrete example of how dangerous a missing ownership check can be.

A Hitchhiker's Guide to Solana Program Security

The "A Hitchhiker's Guide to Solana Program Security" article provides a perfect example of this vulnerability.

Read the section "Missing Ownership Check". Pay close attention to the admin_token_withdraw example. See how an attacker can create a fake config account to bypass the admin check, and how adding a simple if config.owner != program_id line completely mitigates the attack.

2. Validating Signer Privileges

The second critical check is for signer privileges. Many instructions should only be executable by a specific user or authority. For example, transferring tokens, withdrawing from a vault, or changing your username in a profile. How do we prove that the user who owns the account actually authorized this action?

The answer lies in cryptographic signatures. When a user sends a transaction, they sign it with their private key. The Solana runtime verifies this signature and flags the corresponding public key in the transaction's account list.

Inside your program, the AccountInfo struct has a boolean field called is_signer. If account.is_signer is true, it means the holder of that account's private key signed the transaction.

How to Check for a Signature

Your program must check the is_signer flag for any account that is meant to provide authorization.

// Inside your process_instruction function...
let accounts_iter = &mut accounts.iter();
let user_account = next_account_info(accounts_iter)?;

// The crucial signer check
if !user_account.is_signer {
    msg!("Error: The user account did not sign the transaction.");
    return Err(ProgramError::MissingRequiredSignature);
}

If you require an account to be a signer and this check fails, you must halt execution. Otherwise, anyone could call the instruction on behalf of the user without their permission.

What Happens When You Forget?

Forgetting a signer check is one of the most common and severe vulnerabilities in Solana programming.

A Hitchhiker's Guide to Solana Program Security

Let's return to the security guide for another clear illustration of this vulnerability.

Read the section "Missing Signer Check". In the update_admin example, the code checks if the admin account's public key matches the one in the config, but it fails to check if that admin actually signed the transaction. An attacker could simply supply the correct public key (which is public information) and change the admin to themselves. The fix is to add the if !admin.is_signer check.

Test your understanding!

You are building a "notes" program where a user can create and update a personal note stored in a data account. The process_instruction function receives two accounts:

  1. note_account: The data account where the note is stored. It is owned by your program. Its data structure contains a field authority: Pubkey which stores the public key of the user who is allowed to edit it.
  2. user_account: The account of the user attempting to update the note.

Which of the following validation checks are necessary to safely update the note?

A. Check that note_account is owned by the program.
B. Check that the user_account signed the transaction (is_signer == true).
C. Check that user_account.key matches the authority field stored inside the note_account's data.
D. All of the above.

Show answer

The correct answer is D. All of the above.

Here's why each check is essential:

  • A (Ownership Check): You must ensure you are writing to a note_account that your program actually owns. This prevents you from being tricked into writing to some other program's account.
  • B (Signer Check): You must ensure the person initiating this transaction has proven their identity by signing it. This prevents anyone from calling the instruction on behalf of the user.
  • C (Authority Check): Just because someone signed the transaction doesn't mean they are the correct person. You must check that the signer's public key matches the authorized authority stored in the note's data. This prevents User B from updating User A's note.

Without all three checks, your program is vulnerable.

Conclusion

In this lesson, you've learned the two most fundamental security checks in native Solana development. These aren't optional best practices; they are absolute requirements for building safe programs on a blockchain that operates on an attacker-controlled model.

Let's summarize the key takeaways:

  • Solana is Attacker-Controlled: Never trust the accounts or data passed to your program. Your program must validate everything.
  • Validate Account Ownership: Always check that any data account your program intends to modify is owned by your program. Use the check if *account.owner != program_id.
  • Validate Signer Privileges: For any privileged action (like spending funds or changing authorized data), you must verify that the authority account has signed the transaction. Use the check if !account.is_signer.

You now know how to read account data and, just as importantly, how to validate the accounts themselves. With these skills, you're ready for the next logical step.

In our next lesson, we will learn how to serialize and write data to an account's data buffer. Having read data, validated permissions, and performed some logic, the final step is to save the new state back to the blockchain.

Can't find a good explanation? Sign up and we'll make it for you

Sign up