NFTBridge
60,000 USDC
View results
Submission Details
Severity: high
Valid

`Tokens` Are Automatically Whitelisted Upon Creation And Binding Even When `_whiteListEnabled == false`

Summary

In the withdrawTokens function, if the collectionL1 parameter is set to address(0), an ERC721Bridgeable contract is deployed, and the resulting address is automatically whitelisted. This logic does not consider the situation when _whiteListEnabled == false (i.e., when the whitelist mechanism is disabled), which leads to unintended tokens being whitelisted.

If _whiteListEnabled is later changed to true to restrict token bridging, the previously whitelisted tokens will remain whitelisted, potentially enabling malicious or unintended tokens to be bridged without restriction. Additionally, adding a large number of tokens to the _collections array could cause Out-of-gas issues due to excessive iteration during whitelist management.

Vulnerability Details

The _whiteListEnabled and white_list_enabled variables on Ethereum and Starknet, respectively, indicate whether the whitelist mechanism is active.

// Ethereum
bool _whiteListEnabled;
// Stark
// White list enabled flag
white_list_enabled: bool,

If these variables are false, the whitelist check is bypassed, and all tokens are allowed to bridge across chains by default:

function _isWhiteListed(
address collection
) internal view returns (bool) {
return !_whiteListEnabled || _whiteList[collection];
}
fn _is_white_listed(self: @ContractState, collection: ContractAddress) -> bool {
let enabled = self.white_list_enabled.read();
if (enabled) {
let (ret, _) = self.white_listed_list.read(collection);
return ret;
}
true
}

It is pretty sure that _whiteListEnabled or white_list_enabled should be set to be the same to apply same criteria for consistent cross-chain operations, otherwise the uni-directional cross-chain would cause tokens to be stuck.

The problem arises in the withdrawTokens function, where if collectionL1 == address(0), a new ERC721Bridgeable contract is deployed, and the resulting address is automatically added to the whitelist:

if (collectionL1 == address(0x0)) {
if (ctype == CollectionType.ERC721) {
collectionL1 = _deployERC721Bridgeable(
req.name,
req.symbol,
req.collectionL2,
req.hash
);
// update whitelist if needed
@=> _whiteListCollection(collectionL1, true);
} else {
revert NotSupportedYetError();
}
}

This is the same in bridge.cairo:

let (already_white_listed, _) = self.white_listed_list.read(l2_addr_from_deploy);
if already_white_listed != true {
_white_list_collection(ref self, l2_addr_from_deploy, true);
self.emit(CollectionWhiteListUpdated {
collection: l2_addr_from_deploy,
enabled: true,
});
}

This logic automatically adds the newly deployed collection to the whitelist regardless of the _whiteListEnabled or white_list_enabled values. As a result, even if _whiteListEnabled is later changed back to true, these tokens will remain in the whitelist, which is unintended.

Consider a following scenario:

  1. The _whiteListEnabled on Ethereum and white_list_enabled on Starknet are both set to false. This allows any token to be bridged without restriction. A malicious token is bridged from Starknet to Ethereum. Since the whitelist is disabled, no checks are performed, and the token is accepted.

  2. During the withdrawTokens process, if collectionL1 == address(0), an ERC721Bridgeable contract is deployed on Ethereum. This new collectionL1 address is automatically whitelisted, regardless of the _whiteListEnabled status. The malicious token’s collection is now part of the whitelist unintentionally.

  3. Later, due to security concerns, the contract owner re-enables the whitelist mechanism by setting _whiteListEnabled = true on Ethereum and white_list_enabled = true on Starknet. However, since the malicious token’s collection was already added to the whitelist, it remains whitelisted even with the whitelist mechanism now enabled.

  4. The malicious token can still be bridged back because it remains on the whitelist. This undermines the security purpose of re-enabling the whitelist.

Consider another scenario:

  1. The _whiteListEnabled on Ethereum and white_list_enabled on Starknet are both set to false. This allows any token to be bridged without restriction.

  2. A malicious actor bridges 1,000 different tokens from Starknet to Ethereum. Since the whitelist is disabled, all tokens are accepted without any checks.

  3. During the withdrawTokens process, for each of the 1,000 tokens, if collectionL1 == address(0), an ERC721Bridgeable contract is deployed on Ethereum for each token. Each of these 1,000 new collectionL1 addresses is automatically whitelisted, regardless of the _whiteListEnabled status.

  4. The whitelist array _collections now contains 1,000 new entries, which were automatically added due to the disabled whitelist mechanism. This massive addition of collections dilutes the whitelist array, making it cumbersome and inefficient to manage.

  5. The _collections array is iterated over in functions like _whiteListCollection. With the sudden influx of 1,000 new entries, these iterations become costly in terms of gas. As the number of entries grows, operations that involve iterating over the whitelist could run into out-of-gas errors, causing critical functions to fail.

    A brief PoC is shown below:

function test_withdrawTokensCouldAddWhitelist() public {
// Build the request and compute it's "would be" message hash.
felt252 header = Protocol.requestHeaderV1(CollectionType.ERC721, false, false);
Request memory req = buildRequestDeploy(header, 888, bob);
uint256[] memory reqSerialized = Protocol.requestSerialize(req);
bytes32 msgHash = computeMessageHashFromL2(reqSerialized);
// The message must be simulated to come from starknet verifier contract
// on L1 and pushed to starknet core.
uint256[] memory hashes = new uint256[](1);
hashes[0] = uint256(msgHash);
IStarknetMessagingLocal(snCore).addMessageHashesFromL2(hashes);
IStarklane(bridge).enableWhiteList(false);
address collection = IStarklane(bridge).withdrawTokens(reqSerialized);
IStarklane(bridge).enableWhiteList(true);
IStarklane(bridge).isWhiteListed(collection);
}

It is confirmed that the collection is still whitelisted after _whiteListEnabled = true.

│ └─ ← [Return]
├─ [1187] ERC1967Proxy::isWhiteListed(ERC1967Proxy: [0xA1BeA0a8a206D2d676465E18feE61B59749C531d]) [staticcall]
│ ├─ [789] Starklane::isWhiteListed(ERC1967Proxy: [0xA1BeA0a8a206D2d676465E18feE61B59749C531d]) [delegatecall]
│ │ └─ ← [Return] true
│ └─ ← [Return] true

Impact

  1. Unintended Token Whitelisting: Tokens that should not be whitelisted may remain whitelisted, even after the whitelist mechanism is re-enabled, leading to potential security risks.

  2. Out-of-Gas Issues: Excessive additions to the _collections array could cause out-of-gas issues during whitelist management, particularly when iterating through the _collections array in the _whiteListCollection function.

Tools Used

Manual

Recommendations

To mitigate this issue, it is recommended to check the status of _whiteListEnabled and white_list_enabled before adding deployed tokens to the whitelist. When _whiteListEnabled == false, tokens should not be automatically added to the whitelist.

Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-collections-always-withelisted-on-both-chain-withdraw-impossible-collections-array-will-be-OOG

Likelyhood: High, once the whitelist option is disabled, collections will grow. Impact: High, withdraw won’t be possible because of Out-Of-Gas.

Appeal created

bladesec Auditor
9 months ago
jesjupyter Submitter
9 months ago
ge6a Auditor
9 months ago
n0kto Lead Judge
9 months ago
n0kto Lead Judge 8 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-collections-always-withelisted-on-both-chain-withdraw-impossible-collections-array-will-be-OOG

Likelyhood: High, once the whitelist option is disabled, collections will grow. Impact: High, withdraw won’t be possible because of Out-Of-Gas.

Support

FAQs

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