Platform: Code4rena
Start Date: 04/03/2024
Pot Size: $140,000 USDC
Total HM: 19
Participants: 69
Period: 21 days
Judge: 0xean
Total Solo HM: 4
Id: 343
League: ETH
Rank: 40/69
Findings: 2
Award: $211.06
π Selected for report: 0
π Solo Findings: 0
π Selected for report: josephdara
Also found by: Aymen0909, Fassi_Security, MrPotatoMagic, Shield, grearlake, iamandreiski, josephdara, ladboy233, lanrebayode77, t0x1c
177.522 USDC - $177.52
The owner or the bridge_watchdog
can ban an address by calling Bridge.banAddress()
:
function banAddress( address _addr, bool _ban ) external onlyFromOwnerOrNamed("bridge_watchdog") nonReentrant { if (addressBanned[_addr] == _ban) revert B_INVALID_STATUS(); addressBanned[_addr] = _ban; emit AddressBanned(_addr, _ban); }
In Bridge.processMessage
, there is a check that checks if the address in the _message.to
field is banned. If it is banned, the message status gets updated to DONE
;
function processMessage() { //.. code ommited if ( _message.to == address(0) || _message.to == address(this) || _message.to == signalService || addressBanned[_message.to] ) { refundAmount = _message.value; _updateMessageStatus(msgHash, Status.DONE); } //.. code ommited }
After setting the message to status DONE
, a refund will happen to either the msg.sender
or the refundTo
address alongside a small fee for the msg.sender
.
function processMessage() { //.. code ommited address refundTo = _message.refundTo == address(0) ? _message.destOwner : _message.refundTo; if (msg.sender == refundTo) { refundTo.sendEther(_message.fee + refundAmount); } else { msg.sender.sendEther(_message.fee); refundTo.sendEther(refundAmount); } //.. code ommited
This means that it should not be possible to call _invokeMessageCall
onto a banned message since this is reserved for the else
branch after the if-branch
where the checks happened for the banned addresses:
function processMessage() { //.. code ommited if ( _message.to == address(0) || _message.to == address(this) || _message.to == signalService || addressBanned[_message.to] ) { refundAmount = _message.value; _updateMessageStatus(msgHash, Status.DONE); -> } else { uint256 gasLimit = msg.sender == _message.destOwner ? gasleft() : _message.gasLimit; if (_invokeMessageCall(_message, msgHash, gasLimit)) { _updateMessageStatus(msgHash, Status.DONE); } else { _updateMessageStatus(msgHash, Status.RETRIABLE); } } //.. code ommited
However, it is still possible to call _invokeMessageCall
onto a banned address.
Consider the following:
function processMessage() { //.. code ommited } else { uint256 gasLimit = msg.sender == _message.destOwner ? gasleft() : _message.gasLimit; if (_invokeMessageCall(_message, msgHash, gasLimit)) { _updateMessageStatus(msgHash, Status.DONE); } else { -> _updateMessageStatus(msgHash, Status.RETRIABLE); } //.. code ommited
_message.to
gets banned by the bridge_watchdog
:function banAddress( address _addr, bool _ban ) external onlyFromOwnerOrNamed("bridge_watchdog") nonReentrant { if (addressBanned[_addr] == _ban) revert B_INVALID_STATUS(); addressBanned[_addr] = _ban; emit AddressBanned(_addr, _ban); }
_message.to
got banned, it should not be possible to call _invokeMessageCall
on _message.to
._invokeMessageCall
because there are no checks for banned messages in retryMessage
:function retryMessage( Message calldata _message, bool _isLastAttempt ) external nonReentrant whenNotPaused sameChain(_message.destChainId) { if (_message.gasLimit == 0 || _isLastAttempt) { if (msg.sender != _message.destOwner) revert B_PERMISSION_DENIED(); } bytes32 msgHash = hashMessage(_message); if (messageStatus[msgHash] != Status.RETRIABLE) { revert B_NON_RETRIABLE(); } if (_invokeMessageCall(_message, msgHash, gasleft())) { _updateMessageStatus(msgHash, Status.DONE); } else if (_isLastAttempt) { _updateMessageStatus(msgHash, Status.FAILED); } emit MessageRetried(msgHash); }
This results in banned addresses still being able to be interacted with, which should not be the case since _invokeMessageCall
is strictly prohibited from being called on these banned addresses.
Manual Review
Handle retryMessage
calls to banned addresses in the same way they are handled inside processMessage
. That means to send a refund.
Timing
#0 - c4-pre-sort
2024-03-28T00:43:03Z
minhquanym marked the issue as primary issue
#1 - c4-pre-sort
2024-03-28T00:48:10Z
minhquanym marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-03-28T18:59:01Z
minhquanym marked the issue as duplicate of #298
#3 - c4-judge
2024-04-09T18:23:36Z
0xean changed the severity to QA (Quality Assurance)
#4 - c4-judge
2024-04-10T10:50:55Z
0xean marked the issue as grade-b
#5 - c4-judge
2024-04-12T14:03:24Z
This previously downgraded issue has been upgraded by 0xean
#6 - c4-judge
2024-04-12T14:04:41Z
0xean marked the issue as satisfactory
π Selected for report: josephdara
Also found by: Aymen0909, Fassi_Security, MrPotatoMagic, Shield, grearlake, iamandreiski, josephdara, ladboy233, lanrebayode77, t0x1c
177.522 USDC - $177.52
The owner or the bridge watchdog can suspend multiple messages by calling suspendMessages
:
function suspendMessages( bytes32[] calldata _msgHashes, bool _suspend ) external onlyFromOwnerOrNamed("bridge_watchdog") { uint64 _timestamp = _suspend ? type(uint64).max : uint64(block.timestamp); for (uint256 i; i < _msgHashes.length; ++i) { bytes32 msgHash = _msgHashes[i]; proofReceipt[msgHash].receivedAt = _timestamp; emit MessageSuspended(msgHash, _suspend); } }
receivedAt
gets set to type(uint64).max
, which results in the following three functions to revert
:
recallMessage
processMessage
In both these functions, when a message is suspended, this happens:
bool
gets set to true
because receivedAt
is type(uint64).max
bool isMessageProven = receivedAt != 0;
isMessageProven
is true
, the next if-statement
does not get executed:if (!isMessageProven) {
receivedAt
is set to type(uint64).max
, this next if-statement
does not get executed:if (block.timestamp >= invocationDelay + receivedAt) {
isMessageProven
is true
, this else-if-statement
does not get executed:} else if (!isMessageProven) {
else-statement
, causing these functions to revert:} else { revert B_INVOCATION_TOO_EARLY(); }
This demonstrates that it should not possible to interact with suspended messages.
However, this is not the case for suspended retriable messages. Consider the following:
message(1)
is RETRIABLE
message(1)
gets suspended by the bridge watchdog due to suspicious activityretryMessage
since nothing checks if receivedAt
is set to type(uint64).max
:function retryMessage( Message calldata _message, bool _isLastAttempt ) external nonReentrant whenNotPaused sameChain(_message.destChainId) { if (_message.gasLimit == 0 || _isLastAttempt) { if (msg.sender != _message.destOwner) revert B_PERMISSION_DENIED(); } bytes32 msgHash = hashMessage(_message); if (messageStatus[msgHash] != Status.RETRIABLE) { revert B_NON_RETRIABLE(); } if (_invokeMessageCall(_message, msgHash, gasleft())) { _updateMessageStatus(msgHash, Status.DONE); } else if (_isLastAttempt) { _updateMessageStatus(msgHash, Status.FAILED); } emit MessageRetried(msgHash); }
The impact can be big, if a bridge watchdog manages to catch a suspicious transaction in their RETRIABLE
status and suspends it, then the malicious user can still retry it and execute it.
Manual Review
Inside retryMessage
, check if the receivedAt
value is set to type(uint64).max
. If this is true, revert.
Context
#0 - c4-pre-sort
2024-03-28T04:26:46Z
minhquanym marked the issue as duplicate of #273
#1 - c4-pre-sort
2024-03-28T18:59:12Z
minhquanym marked the issue as duplicate of #298
#2 - c4-judge
2024-04-09T18:23:36Z
0xean changed the severity to QA (Quality Assurance)
#3 - c4-judge
2024-04-10T10:50:52Z
0xean marked the issue as grade-b
#4 - c4-judge
2024-04-12T14:03:24Z
This previously downgraded issue has been upgraded by 0xean
#5 - c4-judge
2024-04-12T14:04:37Z
0xean marked the issue as satisfactory
π Selected for report: MrPotatoMagic
Also found by: 0x11singh99, DadeKuma, Fassi_Security, JCK, Kalyan-Singh, Masamune, Myd, Pechenite, Sathish9098, Shield, albahaca, alexfilippov314, cheatc0d3, clara, foxb868, grearlake, hihen, imare, joaovwfreire, josephdara, ladboy233, monrel, n1punp, oualidpro, pa6kuda, pfapostol, rjs, slvDev, sxima, t0x1c, t4sk, zabihullahazadzoi
33.5408 USDC - $33.54
https://github.com/code-423n4/2024-03-taiko/blob/a30b5b6afd121e4de8ceff7165a2091e62194992/packages/protocol/contracts/automata-attestation/utils/RsaVerify.sol#L99-L110 https://github.com/code-423n4/2024-03-taiko/blob/a30b5b6afd121e4de8ceff7165a2091e62194992/packages/protocol/contracts/automata-attestation/utils/RsaVerify.sol#L247-L258
In the library RsaVerify.sol
, the function pkcs1Sha256
is used to verify a a PKCSv1.5 SHA256 signature:
function pkcs1Sha256( bytes32 _sha256, bytes memory _s, bytes memory _e, bytes memory _m ) internal view returns (bool) { //.. code omitted }
Another function in RsaVerify.sol
, which verifies a PKCSv1.5 SHA1 signature, is the function pkcs1Sha1
:
function pkcs1Sha1( bytes20 _sha1, bytes memory _s, bytes memory _e, bytes memory _m ) internal view returns (bool) { //.. code omitted }
Both these functions use the same assembly block to make a staticcall
:
assembly { pop( staticcall( sub(gas(), 2000), 5, add(input, 0x20), inputlen, add(decipher, 0x20), decipherlen ) ) }
The gas used for the staticcall
is calculated by sub(gas(), 2000)
.
However, if there is not enough gas left to make the static call, the execution of pkcs1Sha256()
or pkcs1Sha1()
will continue because there is no check for the return value of the static call being 0
.
Both pkcs1Sha256
andpkcs1Sha1
are either directly or indirectly called inside:
SigVerifyLib.verifyRS256Signature
SigVerifyLib.verifyRS1Signature
SigVerifyLib.verifyCertificateSignature
SigVerifyLib.verifyAttStmtSignature
This means that signatures will be either wrongfully validated or wrongfully invalidated, which impacts the system's core workings since signature validation is one of the key building blocks of a blockchain.
Manual Review
Add a check that requires the return value of the staticcall to be true
.
Context
#0 - c4-pre-sort
2024-03-28T17:30:50Z
minhquanym marked the issue as sufficient quality report
#1 - smtmfft
2024-04-03T01:14:59Z
I think in normal user case this return value should be checked as you pointed out. However, actually all of these functions are in dead code path because in taiko, we use the ecdsa verification for it is the only supported certification type. so in theory none of them will be called. I remember we deleted them when we check the code coverage some times ago, but it maybe later than audit version submition.
#2 - dantaik
2024-04-03T03:52:51Z
I think this is a valid report the the severity is low.
#3 - c4-sponsor
2024-04-03T03:52:59Z
dantaik (sponsor) acknowledged
#4 - c4-sponsor
2024-04-03T03:53:02Z
dantaik marked the issue as disagree with severity
#5 - 0xean
2024-04-09T13:30:34Z
After reviewing the code, I don't see this used anywhere currently. So there is no impact and therefore will mark as QA
#6 - c4-judge
2024-04-09T13:30:38Z
0xean changed the severity to QA (Quality Assurance)
#7 - c4-judge
2024-04-10T10:50:45Z
0xean marked the issue as grade-b