Introduction

A new smart contract weakness was recently discovered by Steve Marx. As per the title of this article, the bug can result in a hash collision on functions with multiple variable-length arguments. Let’s take a look at an example to see the vulnerability in action.

Created by Steve Marx.

As we can see in the contract, if the addUsers function is called by an admin, arrays of admins and regularUsers are added to mappings of isAdmin and isRegularUser . If the function is not called by an admin, it can be relayed with an admins signature.

Problem

The vulnerability can be found on line 15 with the use of abi.encodePacked() . The problem lies in the way that abi.encodePacked() manages its parameters. The following two statements return the same value, even though the parameters are unique.

Different parameters can result in the same return value.

Given that different parameters can return the same value, an attacker could exploit this by modifying the position of elements in a previous function call to effectively bypass authorization. For example, if an attacker saw addUser([addr1, addr2], [addr3, <attacker’s address>, addr4], sig) , they could call addUser([addr1, addr2, addr3, <attacker’s address>], [addr4], sig) . Since the return values are the same, the signature will still match, making the attacker an admin. Though the contract should have proper replay protection, an attacker can still bypass this by front-running.

Remediation

There are a few different remediation's we can take to prevent this vulnerability. The first option is to not allow arrays as parameters, instead of passing a single value. Another option would be to only allow arrays of fixed lengths, so the positions cannot be modified. Finally, we can avoid this vulnerability by simply opting for the use of abi.encode() instead of abi.encodePacked() .

Add only one user at a time.

Allow only fixed length arrays of users.

Use abi.encode() instead of abi.encodePacked()

References