The Gas Station Network (GSN) helps to execute gas-free transactions, but an incorrect implementation of its functionalities could make any project extremely exploitable. We describe an issue we addressed at Ackee Blockchain during a security assessment of one of our clients.

If we look at GSN documentation a GSN compatible project needs to replace all occurrences of msg.sender with _msgSender function.

https://docs.opengsn.org/contracts/#receiving-a-relayed-call

This step is generating a possible risk because with Solidity structure (msg.sender) every developer can rely on its value. But it’s replaced by a custom function (_msgsender()) with an arbitrary body (depends on implementation) where we can’t say the same.

    function _msgSender() internal override virtual view returns (address ret) {
        if (msg.data.length >= 20 && isTrustedForwarder(msg.sender)) {
            // At this point we know that the sender is a trusted forwarder,
            // so we trust that the last bytes of msg.data are the verified sender address.
            // extract sender address from the end of msg.data
            assembly {
                ret := shr(96,calldataload(sub(calldatasize(),20)))
            }
        } else {
            ret = msg.sender;
        }
    }

https://github.com/opengsn/gsn/blob/f9d5b4a3239f226a7d70d2be3f230ccb21d50763/packages/contracts/src/BaseRelayRecipient.sol

As can be seen on code snippet, if:

  • transaction msg.data length is higher or equal 20, :heavy_check_mark:
  • and msg.sender is a trusted forwarder, :question:

it will return the last 20 bytes of msg.data.

But there can be literally anything!

We’ve discovered this during an audit where the project had setTrustedForwarder function set as a public, so anyone could have set up anyone as a trusted forwarder.

We have created an exploit that was based on altering the msg.data. Especially, appending the victim’s address to the end, so _msgSender would return a different address than is the original caller.

function executeAttack(address VICTIM_ADDRESS) public returns (bool success, bytes memory returndata) {

    ...

    bytes memory data_extension = abi.encodePacked(VICTIM_ADDRESS);
    bytes memory data = abi.encodePacked(data_base, data_extension);
    (success, returndata) = address(mainContract).call(data);
}

This exploit allowed us to act as any user we wanted and additionally thanks to the architecture of withdrawal functions of the project also stealing funds from any user of the contract.

The issue was immediately reported and hotfixed by the team by using onlyOwner modifier on setTrustedForwarder function.

Is it enough?

The vulnerability still exists. The owner of the contract could be an initiator of the attack (consciously or by mistake), so there is still some risk. Even the owner shouldn’t have such a big power, otherwise it should be announced to contract users.

To sum up, actual design of _msgSender function from GSN is quite unhappy, because even when it is well implemented, it can in some cases significantly affect trust model of the contract.