Finding Missing Auth bugs in EOSIO Smart Contracts

This is a series of blog posts in which I’ll cover some common EOSIO smart contract vulnerabilities and then explain how we can use our EOSIO vulnerability scanner, Inspect, to perform static analysis to uncover these vulnerabilities.

This week, I look at Missing Authorisations.

The vulnerability

In EOSIO, a developer can use two functions to control authorisation of smart contract actions by checking whether the declared authorisation of the action equals the account that should be able to run the action. These are:

  • require_auth(account [, permission])
  • has_auth(account)

Due to missing authorisation controls, a vulnerable smart contract grants authorisation to untrusted accounts such as malicious parties, to:

  • access/modify privileged smart contract resources e.g. smart contract tables,
  • Call functions of other contracts on behalf of the vulnerable contract, or
  • Perform mission-critical contract actions e.g. token withdrawals.


Let’s look at an example of where a smart contract is setup such that it is paying for EOSIO RAM (which is what EOSIO calls the on chain database) storage costs, in such a scenario every write to the database should be checked to ensure that the entity calling the action that contains the database write has the proper authorisation to do so. Otherwise incorrect data may be written, or a RAM filling attack (see severity below) may occur.

In the action code below, despite there being a require_auth() call prior to the emplace call (which writes data to the _users table), the user is not the RAM payer, allowing any user to write to the database using the contract account's own RAM making the contract vulnerable to a RAM filling attack.

ACTION emplaceself1(name username, const std::string &display_name) {
 // unsafe
 require_auth(username);
 // contract is paying for RAM, requires checking get_self() auth, otherwise RAM filling attack
 _users.emplace(get_self(), [&](auto &new_user) {
   new_user.username = username;
   new_user.display_name = display_name;
 });
}
Unsafe smart contract action.

Modifying the action so that the user pays for RAM, makes the action immune to a RAM filling attack (as the user is no longer using the contract’s resources for RAM):

ACTION emplaceself2(name username, const std::string &display_name) {
 require_auth(username);
 // safe
 _users.emplace(username, [&](auto &new_user) {
   new_user.username = username;
   new_user.display_name = display_name;
 });
}
Smart contract action modified to make it safe.

Severity

The impact of this type of vulnerability is that a hacker could:

  • Write/modify/delete any data stored in RAM that is not adequately guarded by an appropriate authorisation check,
  • Call functionality that should ostensibly be reserved only for the smart contract’s administrators/privileged users,
  • In EOSIO RAM is not free so a hacker can fill up a smart contract’s RAM with spurious data and exhaust the dAPP’s resources leading to a denial of service for legitimate users.

Finding the vulnerability with Inspect

Inspect employs a technique called Static analysis. Static analysis is performed by reasoning about a computer program’s source-code, or some intermediate representation, without actually executing it.

With Inspect we perform static analysis on the WASM binary code, this is decompiled and then lifted into a proprietary intermediate representation (IR). Inspect then takes the IR, represents it in a database which we can mine for patterns of known vulnerabilities.

Running Inspect on a smart contract containing the above smart contract actions gives the following result:

Auth Violation: ✘
Action: emplaceself1    Insn: ($fel) = db_store_i64($feb, $fec, $fef, $feh, $fej, $fek)

The first action is correctly flagged as violating the Authorisation check. As Inspect operates on the WASM byte code, it reports the violation occurring at the EOSIO intrinsic, db_store_i64(), that corresponds to the emplace() call.

The second action is not reported as it is safe.

In a real analysis scenario, the next step would be to correlate this result with the source code and confirm that this is truly a vulnerability.

What else? What vulnerabilities would you like me to cover next?