CosmWasm security spotlight #3

JCsec
Oak Security
Published in
5 min readOct 9, 2023

Are you a CosmWasm Smart Contract developer? A random Cosmonaut peeking into this cool tech? A Solidity auditor looking for new knowledge? Then keep reading!

There are loads of great content about security findings and auditing of Solidity Smart Contracts. Sadly, that is not the case for CosmWasm… yet!

Address validation and normalization

Handling addresses is a key piece in smart contract composability and, of course, in DeFi. Valid Cosmos SDK, and therefore CosmWasm, addresses follow the Bech32 format, composed of 32 letters and numbers. However, you are probably more familiar with the more human-friendly account address representation that has a different prefix depending on the blockchain such as cosmos, osmo, inj, etc.

This post won’t get into the details of the Bech32 format or its representations; instead, we will showcase the security risks of the lack of address validation and normalization.

There is one distinctive feature you should be aware of. Any valid Cosmos SDK address has indeed two valid representations: all-lowercase and all-uppercase. Let’s take cosmos1892yr6fzlj7ud0kfkah2ctrav3a4p4n060ze8f as an example: it translates to the 395441E922FCBDC6BEC9B76EAC2C7D647B50D66F Bech32 address, now look at COSMOS1892YR6FZLJ7UD0KFKAH2CTRAV3A4P4N060ZE8F… it also points to 395441E922FCBDC6BEC9B76EAC2C7D647B50D66F!

This design decision has security implications when handling addresses in your smart contract. It is not enough to ensure that an address follows a valid format; we must also be consistent with the casing we handle.

If we send some funds to a valid address through the Bank module, it won’t matter which casing of the address we are supplying as long as the address is valid. Ultimately, the all-uppercase and all-lowercase point to the same address. The same happens when we craft the destination address of a CosmosMsg. But what happens if our smart contract stores an address to compare it later? For example, during access control enforcement.

Look at the following function, which aims to transfer “credit” after checking for denied addresses.

pub fn do_transfer(
deps: DepsMut,
info: MessageInfo,
amount: Uint128,
dest: String,
) -> Result<Response, ContractError> {
// Check if addresses are denied
if let Some(is_denied) = DENYLIST.may_load(deps.storage, &dest.to_string()? {
if is_denied {
return Err(ContractError::DeniedRecipient);
}
} else if let Some(is_denied) = DENYLIST.may_load(deps.storage, &info.sender.clone())? {
if is_denied {
return Err(ContractError::DeniedSender);
}
};

// Update sender balance
/** redacted
**/

// Send funds to the destination
/** redacted
**/


Ok(Response::new()
.add_attribute("action", "transfer")
.add_messages(messages))
}

If an invalid address is supplied in dest, the transfer of funds will fail down the line, unnecessarily wasting gas — this could happen if we delete the last character of an address by mistake. Since dest undergoes no normalization either, we don’t know which casing has been provided. If an address has been deny-listed in its lowercase version, anyone could provide the uppercase version to do_transfer to bypass the deny-listing mechanism. This is because MY_ADDRESS != my_address, so direct comparison or storage lookups won’t do it.

As a quick reference, take the below into consideration:

  • Any message sent to an invalid address will fail.
  • info.sender will always return a lowercase address.
  • Native funds transfers through the Bank module will be successful for lowercase and uppercase addresses.
  • CosmosMsg will be successful for lowercase and uppercase addresses.
  • Any CW20 and CW721 transfer to capitalized addresses will fail.
  • Direct comparison of addresses will not match lowercase and uppercase addresses. This includes Rust'sVec<> and cw_storage_plus’s Map<> lookups.

Let’s take a look at the following issues as an example:

[CRITICAL] Finding #1 “Attacker can bypass self-call validation.”

The affected contract implements a validation function to avoid sending user-supplied messages to other protocol contracts. As this contract has a privileged role in the protocol, the mechanism is designed to prevent users from performing privileged actions in other contracts.

However, the function failed to consider that there are two working versions of each address. It compares the destination address of the provided message with the lowercase version of the deny-listed address, which does not match when the destination is all-caps, bypassing the mechanism.

[Minor] Finding #5 “Lack of address validation could lead to locked funds.”

The affected Staking-like contract allows users to stake CW20 tokens on behalf of a different address by adding a DepositFor message inside the Cw20ExecuteMsg::Send one.

The address provided in the beneficiary parameter was not properly validated. If the depositor includes an all-capitalized address in this field by mistake or for some unforeseen reason, those funds will be forever locked in the contract. This happens because when the user holding the address attempts to withdraw their funds, their address won’t match as info.sender always contains the lowercase address.

[Minor] Finding #13 “Lack of address validation might cause errors when using invalid stored addresses”

The contract within scope missed validation on addresses that were later stored for funds transfer and other endeavors. As these were stored without validation, if an incorrect format is provided — e.g. a missing character while copying and pasting — any message sent to these addresses will fail.

Hunt the bug!

Go practice in the fifth challenge of Oak’s CosmWasm Security Dojo, and hunt this bug in a small staking contract. Peeking into the exploit too soon counts as cheating! :P

How to avoid this issue?

The easiest way to avoid this issue is to consistently use deps.api.addr_validate over any user-provided address, as it will check for validation and normalization simultaneously! This should include addresses provided as part of a call’s arguments and more subtle cases like the destination of a CosmosMsg.

In the past, an additional step to lowercase the address was required. After this issue was addressed under CWA-2022–002, the addr_validate function was improved to cover this edge case. Including additional lowercasing of addresses after validation is now unnecessary and a waste of gas; see #10 Unnecessary Conversion to Lowercase in Addresses.

Canonicalization of addresses could also be a valid approach. However, this option is less efficient and more convoluted, so it is only recommended if you have a good reason. See #26 Canonical Address Transformations are Inefficient.

Contact me!

If you have any questions or comments or want to know more about Oak Security and Solidified, feel free to drop a line to @jcr_auditor on Telegram or email me at jcr@oaksecurity.io

Check out my GitHub for further educational content on Smart Contract security and auditing. For example, the CosmWasm security and audit roadmap!

Telegram | Email | Twitter | Github | LinkedIn

Disclaimer! Although most of the reports I will link are of audits I took part in, I will also include others. I do not claim ownership of any kind of the external resources included in these articles :)

--

--

JCsec
Oak Security

Smart Contract security auditor specialized in CosmWasm. Follow me on Twitter @jcsec_audits and Github https://github.com/jcsec-security