Take a look at the following code. How many instructions does the Program have?
The obvious answer is 1 instruction: initialize
.
But if you said 1, you are wrong. In fact, the correct answer is 8 instructions.
Why? Well, Anchor adds 7 instructions to your program to enable IDL uploads. This feature is turned on by default unbeknownst to many developers.
What is an IDL upload? An IDL (Interface Description Language) is a JSON file describing how users may interact with a Solana program. It describes the different instructions that the program implements, along with the required arguments and accounts to call them.
In theory, this JSON file could be stored anywhere. But what the heck do we have a blockchain for? It just makes sense to store the IDL at a predetermined address for each Program on-chain.
Thus, Anchor has implemented a feature where the IDL file can be uploaded to an on-chain account that is derived from the program address and the seed anchor:idl
. This way, users can easily find a program IDL by calculating this account address from the program address.
Now that you know what we are talking about, let's see how this IDL upload feature is implemented. As I have teased earlier, there are 7 “hidden” instructions that are added to Anchor programs:
- IdlCreateAccount (Permissionless, 1-Time) Create the main IDL account at the anchor:idl seed. Can be called exactly once, by anyone. The IDL authority will be whoever called this.
- IdlResizeAccount (IDL Authority) Resize an IDL account
- IdlCloseAccount (IDL Authority) Close an IDL account
- IdlCreateBuffer (Permissionless) Create an IDL buffer account using a pre-allocated system account
- IdlWrite (IDL Authority) Write to an IDL account
- IdlSetAuthority (IDL Authority) Change an IDL account authority
- IdlSetBuffer (IDL Authority) Copy data from an IDL buffer account to the main IDL account
In addition to these instructions, there is also a new Account type, IdlAccount
, which has three fields, authority
, data_len
, and data
:
While there can be many accounts of this type, there is one main IdlAccount
, the one that contains the IDL. Its address is derived from the seed anchor:idl
. Other than the main IdlAccount
, there can be arbitrary IDL Buffer Accounts, which are IdlAccounts
at arbitrary addresses. The idea is that a new IDL can be uploaded to a buffer account in chunks, and then the main IdlAccount
can be updated in one step.
Having understood this process, the 7 instructions do make sense: IdlCreateAccount
is used to initialize the main IdlAccount
, and IdlResizeAccount
is used to reallocate it. IdlCreateBuffer
is used to create non-main IdlAccounts
called buffers, and IdlCloseAccount
is used to close buffers, or even the main IdlAccount
. IdlWrite
writes a chunk to a buffer, and IdlSetAuthority
changes the authority of an IdlAccount
. And lastly, IdlSetBuffer
copies a buffer to the main IdlAccount
.
It is the minimal set of instructions to fully manage the IDL upload system including buffers and authority management. And mostly, this is fine.
But I wouldn't make this post if there wasn't a big issue here: The two permissionless functions, IdlCreateAccount
and IdlCreateBuffer
.
Yes, you read that correctly. IdlCreateAccount
, the function which creates the main IDL account for a program, and assigns its IDL authority, is permissionless. Anyone can call it and set themselves as the master of this program's IDL. The catch is that it can only be called once. This means that in an ideal world, the developer would upload their contract and immediately call this instruction to set up the IDL account. But reality is different. There are countless programs on Solana's mainnet that have Anchor's IDL upload feature enabled (it's a default), but have never called this instruction. What does this mean for affected projects? It means that an attacker can prevent these projects from uploading an IDL, and what's even worse, it means that an attacker can upload a malicious IDL, which can be used to rug anyone who tries to use the on-chain IDL. (Read more about malicious IDL attacks in our next blogpost). I call this type of attack an IDL Takeover Attack. Note that this is not a new issue, and in fact it has been known by the Anchor developers for multiple years.
https://github.com/coral-xyz/anchor/issues/444#issuecomment-957056279
Another version of this attack simply attacks front-end tooling such as explorers to show incorrect information. For example, an explorer may use the on-chain IDL to determine that a certain instruction signature is called “deposit”, and thus it will show the instruction as a “deposit” instruction. However, a malicious IDL may change the name shown to “accretion.xyz” and generate free advertisement whenever someone views a transaction involving this instruction. I have generated such a fake IDL for my own Ore mining program. The result: An instruction which should be “Mine” is shown as “Accretion.xyz”.
(also, I’ve renamed the first account in the IDL above to “The Boss” instead of “Authority”). To prove my point, I’ve uploaded this IDL from an account that was completely unrelated to the program, and has no special authority. Anyone could have made the explorer show any labels for my program.
The second instruction that I consider slightly dangerous is IdlCreateBuffer
. By itself it is harmless. But it enables a powerful primitive for exploiting type confusion (sometimes called Type Cosplay) attacks in Solana programs. The prerequisite is that your program has another instruction that is vulnerable to type confusion. This means that this other vulnerable instruction loads an account without checking the account's discriminator, only checking the account owner. Such a vulnerability is often only exploitable when we have another account type for the program that we can legitimately pass as the other. This means it should have ideally the same length, and arbitrarily controllable data.
IdlCreateBuffer
lets anyone create accounts owned by your program that have almost completely arbitrary contents, with an almost arbitrary account length. Remembering the layout of the IDL Account, we see that an attacker has almost full control over the contents. They can set the authority to arbitrary bytes, and the data are arbitrary anyways. Only the data_len
field has to be the real length of the following data, and can not be fully arbitrary.
Now imagine that we have a vulnerable function that expects a given account to have the following layout
The vulnerable function would load this Account, checking that the account owner is the program, and that the program has the correct size, but not checking the account discriminator. To exploit this, an attacker would need another account type within the same program with the same length, and the ability to control the field contents, which is rarely the case. However, using IdlCreateBuffer
, the attack becomes possible.
Let's see how we would exploit this:
Create a Buffer Account. First, an attacker would create a new account with 84 bytes, initialize it using IdlCreateBuffer
, and set themselves as the authority of this buffer. Because the account size is 84 bytes, data_len
will be 84 minus discriminator length and authority length, thus data_len = 44, or 0x0000002c
. The data at this point will be just zeros. The account will look as follows:
Reinterpreting the same account as a DepositReceipt
, it becomes
Next, we proceed to create a fake deposit receipt for $1M USDC. For this we call the IdlSetData
function, and set the data to the USDC mint, and $1M USDC. The Account content becomes:
Next, the attacker grinds a vanity key that ends on 0x2c000000
(because of little endianness). We'll just assume the mined vanity key is hex 0xBBBBBB...BBBB2c000000
. In our last step, we will create a fake timestamp (0x11223344
) and write the first 28 bytes of the vanity key by changing the 32 byte Buffer Authority. We end up with the following account, which is both an IdlAccount (except for the discriminator), and a valid Deposit receipt for $1M USDC for our vanity address.
An attacker could now use the fake DepositReceipt
to exploit the function which we assumed would be vulnerable to a type confusion attack.
All this goes to show that IDL buffers give attackers a very potent tool to exploit any type confusion vulnerability in Anchor contracts that have IDL uploads enabled, because the attacker can write almost arbitrary data at an arbitrary length into this account.
Again, I would like to emphasize that without a completely separate type confusion vulnerability, this IDL function is not dangerous.
To protect against such attacks, the only good way is to disable the anchor on-chain IDL feature altogether. However, if you're confident that you have no type confusion vulnerabilities in your contract, because every single one of your instruction correctly enforces all account types, it is sufficient to make sure that you claim the main IDL account before anyone else does.
If you're affected by a malicious IDL attack, or someone else claiming your IDL account, your only recourse is to perform a program upgrade with a new custom instruction which transfers the IDL authority to your account.
In general, we consider this a design issue with Anchor. We have contacted their team and recommended removing the Instructions per default, and ideally restricting the IdlCreateAccount
instruction to the program authority, or another account that's hardcoded during compilation. The Anchor team has told us that they are already aware of the issue and that fixes will be coming in version v0.31.0, which is on it’s way.