Secure the Solana Ecosystem (4) — Account Validation
In the previous blog, we discussed how to upgrade a program. In this post, we will introduce the access control related problems, which is one of the most common and basic security topics in the area of DeFi.
In Solana, each program exports one single entrypoint, which is defined with
entrypoint!. Different from Ethereum, clients can only invoke one single function defined as the entrypoint, which is usually called
process_instruction. The entrypoint function receives three parameters. They are the program ID of the smart contract, the accounts that the program will operate on, and the instruction data. The instruction data specifies which instruction will be invoked. The figure below shows an example. By unpacking the instruction data, different instructions (e.g., Lock, Unlock) are chosen. Thus, the instructions that can be reached from entrypoint are public to everyone and can be executed with specified instruction data.
3. Account Validation
As mentioned, the program receives the accounts that it needs to read or write. It brings two questions with this design. For the accounts to be read, how to guarantee that the data stored in the accounts are trusted. For the accounts to be written, how to guarantee only privileged users can invoke the instructions to write into the accounts. In the following, we illustrate the access control problem . All test codes can be found here.
3.1 Code Review (PrivilegeOwner)
We first define two structs and they are
Config. Only the
key account (line 17) specified in the struct
door can open the created door. However, the door can not be opened when the system state is locked, which is specified in structure
Config (line 81).
As mentioned, the
Config account specifies whether the door can be opened. In this case, there should be only one
Config account in the program. To achieve this, we use PDA to store the data of
Config. After initializing the
Config account, we set the attribute
is_initialized to be
true so that it cannot be initialized again by attackers (line 108 - line 110).
Open() is used to open the doors. The instruction receives several accounts including the
door account to be opened, the
config account, and the
owner account that aims to open the door. To guarantee the door belongs to the program and the configuration is valid, we check the owner of the
door account and the
config account (line 204 -line 205). This prevents malicious users from feeding fake accounts. This answers our first question. To guarantee the account to be read is trusted, we need to check the owner of the account!. Note that only the owner of the
door account can open the door. In this case, we check whether the
owner account is the real owner of the
door and more importantly, whether the instruction is authorized by the owner (line 217 - line 219).
validate_owner(), we first check if the public key of these two accounts are same, and then check the signature of the owner. This answers the second question, to guarantee that only privileged users can invoke the
open instruction, we need to check the owner and the signer of the account. The
close instruction is similar to
open and the details can be found in the code.
We deployed the program on testnet and it can be find in the following link.
All the test transactions are listed below. The whole process of this transaction is
3.2 Attack Transaction
To show the importance of the owner check and the signer check, we use two attack scenarios as examples.
The first one is that the ‘door’ owner try to open the door when the
config is locked. To achieve this, we create a fake
config account in another program, and assign the attribute
is_lock with value
false. The code of the custom program is shown below.
We send the transaction to create the fake
config account, the public key of the fake config account is: 2MtSrbWp24VjPZQcSUkiWrvNro7qqKemVCsh3Yxc8LTy.
Once the fake
config account is created, we feed it into the program (line 423).
The result is shown below, the log prints that
incorrect program id for instruction, which means the
config account's owner must be the program. Thus, the attacker cannot bypass this check.
The second scenario is that a malicious user tries to open the door when the door is unlocked.
In this case, we feed the real owner account to the program(line 419) and send the transaction. The result is shown below.
It prints that
Signature verification failed, which means the real owner must sign the transaction to open the door, so our second attack fails as well.
In Solana, instructions implement specified logic based on different accounts, which are feed by clients or the other programs. Therefore, the proper check on the accounts is rather important.
In this article, we introduce how to properly check the account and use two attack scenarios to illustrate the importance of these checks. Keep following and more articles will be shared.