In February 2022, the Wormhole Solana Bridge was breached and assets worth more than 321 million USD were stolen.
The attacker exploited a vulnerability in the mechanism used to verify signatures for authorization. The implementation expected Solana’s native secp256k1 program to be called within the same transaction sequence just before Wormhole’s own verify signature instruction.
let current_instruction = solana_program::sysvar::instructions::load_current_index(
&accs.instruction_acc.try_borrow_mut_data()?,
);
if current_instruction == 0 {
return Err(InstructionAtWrongIndex.into());
}
// The previous ix must be a secp verification instruction
let secp_ix_index = (current_instruction - 1) as u8;
let secp_ix = solana_program::sysvar::instructions::load_instruction_at(
secp_ix_index as usize,
&accs.instruction_acc.try_borrow_mut_data()?,
)
.map_err(|_| ProgramError::InvalidAccountData)?;
// Check that the instruction is actually for the secp program
if secp_ix.program_id != solana_program::secp256k1_program::id() {
return Err(InvalidSecpInstruction.into());
}
Since Solana version 1.8.1, the load_instructions_at function has been marked as deprecated with the comment:
Unsafe because the sysvar accounts address is not checked, please use load_instruction_at_checked instead
As the comment hints, the load_instructions_at function does not verify that the instruction data it processes originates from the known and authorized Solana sysvar account. This can be observed from the arguments passed to the load_instructions_at function, which only include raw data. Since no account is passed into the function, no account addresses can be validated. And as the instruction account address is not validated to originate from the sysvar instructions, any other account can be passed. The attacker abused this to inject their own fake data mimicking a validation result from the secp256k1 program.
The vulnerable version of the Wormhole Solana Bridge used version 1.7.0 of the Solana libraries. As load_instructions_at wasn’t marked as deprecated until version 1.8.1, no warnings were automatically raised during the build process.
So, the $321 million question is:
How do you find a missing validation check like this?
To answer that question, let us have a look at what is required to provide security for a smart-contract.
An old and commonly accepted definition of security includes the aspects of authentication, authorization, confidentiality, and data integrity. To implement a sane security model, a smart-contract must live up to these aspects.
Authentication
Authentication defines the ability to prove that a user is who he/she claims to be. Without validation of authentication, anyone can claim to be any user, which can grant them unintended privileges.
Account Signing is one of the most basic mechanisms in Solana for authentication. Signatures provided by an account give the user a way to prove that he/she is in possession of the private key for a specific account address. Under the assumption that only the owner of the account knows the private key, a user in possession of the private key must be the authentic owner.
In the case of the Wormhole Solana Bridge, the native secp256k1 program was used to sign a specific block of data included in the transaction. Like the basic account signing validation, the outcome is that the user provides a proof of authenticity in the given context.
Authorization
Authorization defines the ability to only allow intended users to perform privileged actions. Without validation of authorization, any user can perform any action.
Authentication and authorization are often confused with each other. But even though authentication is performed, it only gives proof that the user is who he/she claims to be, not that the user is allowed to perform the requested action.
To achieve authorization in a smart-contract, the authorization privileges must be modeled. Data from a trusted account must somehow refer to the signing account in order to obtain both authentication and authorization.
Confidentiality
Confidentiality defines the ability to keep sensitive data isolated.
However, on the Solana blockchain, all data is publicly available. So, confidentiality is not even an option to consider.
This includes sensitive keys like passwords and private keys that should never be stored on-chain. If a smart-contract needs to sign by itself, Program Derived Addresses can be used instead.
As smart-contracts are binary programs stored as account data on the blockchain, secret keys should never be placed in the code either. Anyone with malicious intentions could reverse-engineer the binary program and extract the secret key.
Data integrity
Data integrity is the ability to trust data and that no unauthorized modification has occurred to the data.
The anatomy of a Solana smart-contract is like a stateless function. The program itself cannot store any data by itself. All data being read and written is done by manipulating the data of the account input.
But what data can the smart-contract trust?
The paranoid answer is none. However, this would render the entire concept of smart-contracts useless. Luckily, we do not need to be that pessimistic.
A smart-contract can trust data that it can validate. And validation can be based on the Solana runtime policies:
Only the owner of the account may change owner.
And only if the account is writable.
And only if the account is not executable.
And only if the data is zero-initialized or empty.
An account not assigned to the program cannot have its balance decrease.
The balance of read-only and executable accounts may not change.
Only the system program can change the size of the data and only if the system program owns the account.
Only the owner may change account data.
And only if the account is writable.
And only if the account is not executable.
Executable is one-way (false->true) and only the account owner may set it.
No one can make modifications to the rent_epoch associated with this account.
With regards to data integrity, only account data owned by a smart-contract can be written by the smart-contract itself. To ensure data integrity, the account owner must be validated as the smart-contract itself. Additionally, the smart-contract must not expose any functionality that allows unintended manipulation of account data owned by the smart-contract.
Data Model
Most functionality requires multiple input accounts. Even though we can trust that the program did write the account data and the user is both authenticated and authorized to perform the requested action, we still need to ensure that the provided accounts are only used in a combination as expected. This is where data modeling comes into play. To verify that only the expected accounts are used in combination, all accounts must somehow refer to each other. As the data model is different from smart-contract to smart-contract, what exactly to validate will also differ.
An example of accounts used in a context as expected is the MintTo instruction of SPL Token program. As input the MintTo instruction requires a mint account, a signing mint authority account, and a token account to receive the minted tokens. The data model for the token account defines a reference to the mint account of which the account holds tokens. If the reference to the mint account was not validated, any tokens can be minted by any mint authority. Likewise, the data model for the mint account defines a reference to the mint authority account. If the reference to the mint authority account was not validated, any signing account could be passed as mint authority.
As mentioned, validating the combination of input accounts differs from smart-contract to smart-contract as it depends on the business logic. A general approach is to verify that all input accounts are referred to by the data model and that data integrity is obtained for accounts holding the data model.
The account relations of the SPL Token MintTo instruction can be illustrated as something like this:
Account relations for MintTo instruction for SPL Token program
If any of the input accounts are not connected to the other accounts in the illustration, no validation prevents an attacker from injecting an unexpected account, which likely leads to unexpected behavior and, in the worst case, an exploitable vulnerability.
The $321 million question
Validation of data integrity is where the Wormhole Solana Bridge failed. The owner of the instruction account was not validated to be the native sysvar account. Thus, the attacker could pass an account containing fabricated data claiming the secp256k1 signature was valid.
So, data integrity was used to fake authenticity allowing validation of authorization to be bypassed.
To identify vulnerabilities such as this, the following aspects must be analyzed for each instruction of the smart-contract:
Authentication
Authorization
Confidentiality
Data integrity
Data model validation
A simplified illustration of the account relations of the Wormhole VerifySignatures instruction shows the missing validation of the instruction account:
Account relations for VerifySignatures instruction for Wormhole Solana bridge
Neither account key nor account owner are validated for the instruction account. This clearly indicates that data integrity is broken.
Comments