Where: https://github.com/SocketDotTech/socket-DL/blob/dev/contracts/socket/SocketDst.sol#L134 Where: https://github.com/SocketDotTech/socket-DL/blob/dev/contracts/socket/SocketDst.sol#L94
This issue allows colluded transmitters to execute malicious commands on any plug connected to socket DL bypassing switchboard validations.
Affected Switchboards: FastSwitchBoard. (Other might be affected as well).
I believe the impact would fit into Message getting corrupted during transit
and Decapacitor unable to validate message
. This could impact all the application built on top of socket's DL.
The general architectural design has a lot of moving parts including attestation of packetIds and the proposal of packetIds on switchboard and socket respectively. Checks should have to be added in place to validate that all the variables connecting those validations have tight links and are closely validated.
Few proposed changes could be
- Add a check between the
remoteSlug
in both packetId and msgId. - Tightly pack the timeout validations in fast switchboard as suggested in my another report. https://gist.github.com/sujithsomraaj/fae9e9515d6e347c61e3ded4d6b2e7c9
Propose a random packetId with a different remoteSlug
, Eg: use BSC (56) on SocketDst.sol
function propose(
bytes32 packetId_,
bytes32 root_,
bytes calldata signature_
) external override
The watchers will trip the path and remove you as transmitters which is totally fine. They're doing what we expect. (if they trip global path, we cannot move further but hey that's griefing).
Wait for the timeoutPeriod to elapse between the proposal time and the current block.timestamp. Now call the execute() which is an open function using a msgId of your choice that maps with the random root you proposed in Step-1.
function execute(
bytes32 packetId_,
ISocket.MessageDetails calldata messageDetails_,
bytes memory signature_
) external override {
if (messageExecuted[messageDetails_.msgId])
revert MessageAlreadyExecuted();
messageExecuted[messageDetails_.msgId] = true;
if (packetId_ == bytes32(0)) revert InvalidPacketId();
if (packetIdRoots[packetId_] == bytes32(0)) revert PacketNotProposed();
- uint32 remoteSlug = _decodeSlug(messageDetails_.msgId); /// Exploitable Place (use a different chain id: eg: optimism)
address localPlug = _decodePlug(messageDetails_.msgId);
PlugConfig storage plugConfig = _plugConfigs[localPlug][remoteSlug];
- make sure your root proposed is equal to the packed message
bytes32 packedMessage = hasher__.packMessage(
remoteSlug,
plugConfig.siblingPlug,
chainSlug,
localPlug,
messageDetails_.msgId,
messageDetails_.msgGasLimit,
messageDetails_.executionFee,
messageDetails_.payload
);
(address executor, bool isValidExecutor) = executionManager__
.isExecutor(packedMessage, signature_);
if (!isValidExecutor) revert NotExecutor();
- the verify should pass since the path is not tripped for optimism & the timeout has elapsed.
_verify(
packetId_,
remoteSlug,
packedMessage,
plugConfig,
messageDetails_.decapacitorProof
);
_execute(
executor,
messageDetails_.executionFee,
localPlug,
remoteSlug,
messageDetails_.msgGasLimit,
messageDetails_.msgId,
messageDetails_.payload
);
}
The transmitter can bypass all checks easily by just adding a random packetId and waiting for the timeperiod to elapse on the switchboard.
The primary vulnerable code is that the packetId's src chain and the msg id's chain are not validated anywhere and the root is validated just against the local state available on the socket contract. Read the root from switchboard to avoid bypassing decapacitor validations as well.