What it does:
This lint checks that for each account referenced in a program, that there is a corresponding owner check on that account. Specifically, this means that the owner field is referenced on that account.
Why is this bad?
The missing-owner-check vulnerability occurs when a program uses an account, but does
not check that it is owned by the expected program. This could lead to vulnerabilities
where a malicious actor passes in an account owned by program X when what was expected
was an account owned by program Y. The code may then perform unexpected operations
on that spoofed account.
For example, suppose a program expected an account to be owned by the SPL Token program. If no owner check is done on the account, then a malicious actor could pass in an account owned by some other program. The code may then perform some actions on the unauthorized account that is not owned by the SPL Token program.
Works on:
- Anchor
- Non Anchor
Known problems:
Key checks can be strengthened. Currently, the lint only checks that the account's owner
field is referenced somewhere, ie, AccountInfo.owner.
Example:
See https://github.com/coral-xyz/sealevel-attacks/blob/master/programs/2-owner-checks/insecure/src/lib.rs for an insecure example.
Use instead:
See https://github.com/coral-xyz/sealevel-attacks/blob/master/programs/2-owner-checks/secure/src/lib.rs for a secure example.
How the lint is implemented:
check_fn:
- for every function defined in the package
- exclude functions generated from macro expansion.
- Get a list of unique and unsafe AccountInfo's referenced in the body
- for each expression in the function body
- Ignore
.clone()expressions as the expression referencing original account will be checked - Check if the expression's type is Solana's
AccountInfo(solana_account_info::AccountInfo) - Ignore local variable expressions (
xwhere x is defined in the functionlet x = y)- Removes duplcate warnings: both
xandyare reported by the lint. reportingyis sufficient. - Also the owner could be checked on
y. reportingxwhich a copy/ref ofywould be false-positive. - Determined using the expression kind (
.kind): expr.kind = ExprKind::Path(QPath::Resolved(None, path)); path.segments.len() == 1
- Removes duplcate warnings: both
- Ignore safe
.to_account_info()expressions.to_account_info()method can be called to convert different Anchor account types toAccountInfo- The Anchor account types such as
AccountimplementOwnertrait: The owner of the account is checked during deserialization - The expressions
x.to_account_info()wherexhas one of following types are ignored:Accountrequires its type argument to implementanchor_lang::Owner.Program's implementation oftry_fromchecks the account's program id. So there is no ambiguity in regard to the account's owner.SystemAccount's implementation oftry_fromchecks that the account's owner is the System Program.AccountLoaderrequires its type argument to implementanchor_lang::Owner.Signerare mostly accounts with a private key and most of the times owned by System Program.Sysvartype arguments checks the account key.
- Ignore
x.to_account_info()expressions called on AnchorAccountInfoto remove duplicates.- the lint checks the original expression
x; no need for checking both.
- the lint checks the original expression
- For each of the collected expressions, check if
owneris accessed or if thekeyis compared- Ignore the
account_exprif any of the expressions in the function is{account_expr}.owner - Ignore the
account_exprifkeyis compared- if there is a comparison expression (
==or!=) and one of the expressions being compared accesses key onaccount_expr:- lhs or rhs of the comparison is
{account_expr}.key(); The key for Anchor'sAccountInfois accessed using.key() - Or lhs or rhs is
{account_expr}.key; The key of SolanaAccountInfoare accessed using.key
- lhs or rhs of the comparison is
- if there is a comparison expression (
- Else
- If the expression is
.to_account_info()and the receiver is a field access on a struct:x.y.to_account_info() - Or If the expression is a field access on a struct
x.y- Then store the struct(x) def id and the accessed field name (y) in
MissingOwnerCheck.account_exprs.
- Then store the struct(x) def id and the accessed field name (y) in
- Else report the expression.
- If the expression is
- Ignore the
check_item: Collect Anchor Accounts structs
- for each item defined in the crate
- If Item is a Struct and implements
anchor_lang::ToAccountInfostrait.- Get the pre-expansion source code and parse it using anchor's accounts parser
- If parsing succeeds
- Then store the struct def id and the resultant AccountsStruct in
MissingOwnerCheck.anchor_accounts
- Then store the struct def id and the resultant AccountsStruct in
- If Item is a Struct and implements
check_crate_post:
- for each account expression in
MissingOwnerCheck.account_exprs- If the struct accessed in the expression is in
MissingOwnerCheck.anchor_accounts- find the
#[account(...)]constraints applied on the accessed field - If any of the following constraints are applied on the field/account
- Then ignore the expression.
- Constraints:
#[account(signer)]- Signer accounts are assumed to be EOA accounts and are ignored.#[account(init, ...)]- init creates a new account and sets its owner to current program or the given program.#[account(seeds = ..., ...)]- Anchor derives a PDA using the seeds. This is essentially akeycheck#[account(address = ...)]- Validates the key of the account.#[account(owner = ...)]- Checks the owner.#[account(executable)]- The account is an executable; All executables are owned byBPFLoaders.
- Else report the expression.
- find the
- If the struct accessed in the expression is in