code-423n4 / 2024-08-wildcat-findings

4 stars 2 forks source link

`AccessControlHooks::onQueueWithdrawal` do not check `market.isHooked` allowing anyone to call the function with arbitrary `hooksData` #26

Closed howlbot-integration[bot] closed 1 month ago

howlbot-integration[bot] commented 2 months ago

Lines of code

https://github.com/code-423n4/2024-08-wildcat/blob/main/src/access/AccessControlHooks.sol#L812-L825 https://github.com/code-423n4/2024-08-wildcat/blob/main/src/access/AccessControlHooks.sol#L704-L704 https://github.com/code-423n4/2024-08-wildcat/blob/main/src/access/AccessControlHooks.sol#L531-L561

Vulnerability details

AccessControlHooks::onQueueWithdrawal do not check market.isHooked allowing anyone to call the function with arbitrary hooksData

Summary

AccessControlHooks::onQueueWithdrawal has no access control in contrast to all other hooks, making it possible for an attacker to call _tryValidateAccess(status, lender, hooksData) with arbitrary hooksData.

  1. By calling onQueueWithdrawal directly, the attacker has access to _tryValidateAccess which can update a lender status through _writeLenderStatus
  2. Thus, any Role Provider listed in _roleProviders mapping can be called by the attacker to get credentials, even those who are not in the pull provider list and might be there only to grant access to specific priviledged users.
  3. The call is executed by the Hook, meaning the attacker is calling the role provider with arbitrary data with msg.sender = hook
  4. The attacker can gain access to credentials and operate into the market unexpectedly

Vulnerability details

If we follow the call path:

  1. User call onQueueWithdrawal with arbitrary hooksData (containing a selected role provider)
  2. _tryValidateAccess is called
  3. Then _tryValidateAccessInner is called
  4. _handleHooksData is called and will return hasValidCredential == true if _tryValidateCredential return true
  5. _tryValidateCredential is called
  6. And finally the role provider is extracted from the hooksData and called
  7. When the caller succesfully get (unauthorized) valid credentials, he can then deposit, which will this time call _writeLenderStatus with canSetKnownLender == true, setting isKnownLenderOnMarket == true for that market.
  8. Finally, once isKnownLenderOnMarket == true for the user, he can transfer market's tokens as the onTransfer hook is skipped

For this to be possible step (7) is crucial, and require the provider to return valid credential to the caller, which is possible depending on the verification method used by that provider. The issue would not exist with an access control on the hook.

Under in normal circumstances, Lenders have 2 ways to validate their credential:

  1. a Role Provider calls grantRole with the Lender address
  2. If Lender do not have valid credentials or valid lastProvider, by calling a hooked market function, the hook will go through a list of PullProvider to try validate his credentials

Thanks to the missing access control on onQueueWithdrawal, a Lender can ask credential to any Provider that are not part of the list above, which might be special Providers that shouldn't be accessed by anyone, and might grant credential to the caller.

Impact

Unrestricted access to any Provider by a Lender to try validate his credentials. Also quoting from the "Attack Ideas" section of the Readme :

Tools Used

Manual review

Recommended Mitigation Steps

diff --git a/src/access/AccessControlHooks.sol b/src/access/AccessControlHooks.sol
index 1124e5b..bbbabea 100644
--- a/src/access/AccessControlHooks.sol
+++ b/src/access/AccessControlHooks.sol
@@ -816,6 +816,8 @@ contract AccessControlHooks is MarketConstraintHooks {
     MarketState calldata /* state */,
     bytes calldata hooksData
   ) external override {
+   HookedMarket memory market = _hookedMarkets[msg.sender];
+   if (!market.isHooked) revert NotHookedMarket();
    LenderStatus memory status = _lenderStatus[lender];
    if (
        !isKnownLenderOnMarket[lender][msg.sender] && !_tryValidateAccess(status, lender, hooksData)

Assessed type

Invalid Validation

d1ll0n commented 1 month ago

This is not an issue (certainly not a high). The reason the other functions check isHooked is because they are state-changing beyond the permissions update. onTransfer and onDeposit have the ability to mark an account as a known lender, while onQueueWithdrawal can only update a lender's credentials (provided they have a valid credential to be updated / retrieved from a pull provider). The updating of credentials for a particular lender is not intended to be restricted in any way.

3docSec commented 1 month ago

Interesting finding.

I will go with L because, while the path is viable, it is so under the condition that the providerAddress provided in the hooks data was previously stored in the _roleProviders mapping, so the caller can't avoid passing by a trusted gatekeeper for their call:

File: AccessControlHooks.sol
534:     RoleProvider provider = _roleProviders[providerAddress];
535:     if (provider.isNull()) return false;
c4-judge commented 1 month ago

3docSec changed the severity to QA (Quality Assurance)

c4-judge commented 1 month ago

3docSec marked the issue as grade-b

c4-judge commented 1 month ago

This previously downgraded issue has been upgraded by 3docSec

c4-judge commented 1 month ago

3docSec marked issue #11 as primary and marked this issue as a duplicate of 11

3docSec commented 1 month ago

While well elaborated, the attack idea here is different than #11 and #83, and quite less likely/impactful because of https://github.com/code-423n4/2024-08-wildcat-findings/issues/26#issuecomment-2393174425 - for this reason I find fair to award only partial credit to this report

c4-judge commented 1 month ago

3docSec marked the issue as partial-50