Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

attacker can perform griefing for process() in PromiseRouter by reverting calls to callback() in callbackAddress #225

Open
code423n4 opened this issue Jun 19, 2022 · 3 comments
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working sponsor acknowledged Technically the issue is correct, but we're not going to resolve it for XYZ reasons

Comments

@code423n4
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/promise/PromiseRouter.sol#L226-L262

Vulnerability details

Impact

process() in PromiseRouter is used for process stored callback function and anyone calls it gets callbackFee and it calls callback() function of callbackAddress. but attacker set a callbackAddress that reverts on callback() and cause process() caller griefing. attacker can perform this buy front running or complex logic.

Proof of Concept

This is process() code:

  /**
   * @notice Process stored callback function
   * @param transferId The transferId to process
   */
  function process(bytes32 transferId, bytes calldata _message) public nonReentrant {
    // parse out the return data and callback address from message
    bytes32 messageHash = messageHashes[transferId];
    if (messageHash == bytes32(0)) revert PromiseRouter__process_invalidTransferId();

    bytes29 _msg = _message.ref(0).mustBePromiseCallback();
    if (messageHash != _msg.keccak()) revert PromiseRouter__process_invalidMessage();

    // enforce relayer is whitelisted by calling local connext contract
    if (!connext.approvedRelayers(msg.sender)) revert PromiseRouter__process_notApprovedRelayer();

    address callbackAddress = _msg.callbackAddress();

    if (!AddressUpgradeable.isContract(callbackAddress)) revert PromiseRouter__process_notContractCallback();

    uint256 callbackFee = callbackFees[transferId];

    // remove message
    delete messageHashes[transferId];

    // remove callback fees
    callbackFees[transferId] = 0;

    // execute callback
    ICallback(callbackAddress).callback(transferId, _msg.returnSuccess(), _msg.returnData());

    emit CallbackExecuted(transferId, msg.sender);

    // Should transfer the stored relayer fee to the msg.sender
    if (callbackFee > 0) {
      AddressUpgradeable.sendValue(payable(msg.sender), callbackFee);
    }
  }

As you can see it calls ICallback(callbackAddress).callback(transferId, _msg.returnSuccess(), _msg.returnData()); and if that call reverts then whole transaction would revert. so attacker can setup callbackAddress that revert and the caller of process() wouldn't get any fee and lose gas.

Tools Used

VIM

Recommended Mitigation Steps

change the code so it won't revert if call to callbackAddress reverts.

@code423n4 code423n4 added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Jun 19, 2022
code423n4 added a commit that referenced this issue Jun 19, 2022
@jakekidd
Copy link
Collaborator

jakekidd commented Jun 25, 2022

If the callback would revert, normally it won't be called.

However, the attacker (griefer) could potentially frontrun the relayer's callback transaction (which is already submitted / in the mempool) and update the state of their callback contract in such a way to cause this subsequent callback to fail. Why would they do that? Nothing is gained, only losses are incurred on both sides.

This seems like it should be invalid, but it also seems like we should be doing a try/catch on principle though. Perhaps the issue is misrepresented here - it's not so much an attack vector as it's 'best practice' / QA issue.

Let's de-escalate to QA issue. Confirming, but would be great to get a second look from @LayneHaber on this.

EDIT: Changed my mind, think the risk level is appropriate.

@jakekidd jakekidd added disagree with severity Sponsor confirms validity, but disagrees with warden’s risk assessment (sponsor explain in comments) sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") and removed disagree with severity Sponsor confirms validity, but disagrees with warden’s risk assessment (sponsor explain in comments) labels Jun 25, 2022
@LayneHaber
Copy link
Collaborator

If you change the message, the following check(s) would fail (since the hash is put onchain when the message is propagated by nomad):

bytes32 messageHash = messageHashes[transferId];
if (messageHash == bytes32(0)) revert PromiseRouter__process_invalidTransferId();

bytes29 _msg = _message.ref(0).mustBePromiseCallback();
if (messageHash != _msg.keccak()) revert PromiseRouter__process_invalidMessage();

An attacker cannot falsify this message because sending messages on the router is restricted via the onlyConnext modifier, so I don't think changing the callbackAddress is a valid attack strategy.

If we extend this concern to any failing callback, then any revert would not be executed. In most cases, this would be caught in offchain simulations meaning the relayer would not submit the transaction (and then nobody could process the callback). This means that integrators must handle failures within their code. This is a common practice when writing crosschain handlers (i.e. nomad handle should not revert as the message cannot be reprocessed, functions called via execute should handle errors unless sending funds to a recovery address is okay, etc.).

Don't think this qualifies as a value leak, but because it is potentially unprocessable will leave the severity at 2 and move the label to "acknowledged"

@LayneHaber LayneHaber added sponsor acknowledged Technically the issue is correct, but we're not going to resolve it for XYZ reasons and removed sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity") labels Jun 30, 2022
@0xleastwood
Copy link
Collaborator

0xleastwood commented Aug 15, 2022

I guess this issue has the same concerns as #220. Even if the onus is on the caller of process to simulate the call beforehand, it seems likely that the transaction could be front-run and prove to be poor user experience for relayers.

I don't think its realistic that relayers would call this function without first simulating the transaction to see if the callback fails, but I guess the bridge user could set the callback address up as a honey pot such that simulated transactions are successful.

It probably makes sense to remove the main culprit of the issue by using a try/catch statement for the ICallback(callbackAddress).callback(transferId, _msg.returnSuccess(), _msg.returnData()); call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working sponsor acknowledged Technically the issue is correct, but we're not going to resolve it for XYZ reasons
Projects
None yet
Development

No branches or pull requests

4 participants