Updated on 2026-08-14
This commit is contained in:
parent
6b770f5d00
commit
f1a4be49b3
13 changed files with 337 additions and 103 deletions
|
|
@ -200,20 +200,33 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
|
|||
verifyContext: Wallet.Model.VerifyContext,
|
||||
): Either<WcPairError, WcPairState.Proposal> = runCatching {
|
||||
val proposalAccountNetwork = associateNetworksDelegate.associateAccounts(sessionProposal)
|
||||
// Display URL: shown to the user and logged to analytics. Reown's verified origin when
|
||||
// present, otherwise its `verify.walletconnect.org` fallback. NOT trustworthy for
|
||||
// security checks: when validation is INVALID, getDappOriginUrl returns the dApp-claimed
|
||||
// origin (so the UI can show what was claimed), which a scam dApp can spoof.
|
||||
val displayUrl = verifyContext.getDappOriginUrl()
|
||||
val verificationInfo = when {
|
||||
verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE
|
||||
verifyContext.isScam == true -> CheckDAppResult.UNSAFE
|
||||
else -> blockAidVerifier.verifyDApp(DAppData(sessionProposal.url)).getOrElse { error ->
|
||||
TangemLogger.withTag(WC_TAG).e("Failed to verify DApp ${sessionProposal.name}", error)
|
||||
CheckDAppResult.FAILED_TO_VERIFY
|
||||
// BlockAid is scanned only against the Reown-verified origin (validation == VALID
|
||||
// guarantees Reown confirmed origin matches the dApp's registered domain).
|
||||
// For UNKNOWN we have no trustworthy URL: passing a dApp-claimed URL would let an
|
||||
// impersonator (e.g. a scam claiming metadata.url=dydx.trade) inherit its target's
|
||||
// BlockAid verdict.
|
||||
verifyContext.validation == Wallet.Model.Validation.VALID -> {
|
||||
blockAidVerifier.verifyDApp(DAppData(verifyContext.origin)).getOrElse { error ->
|
||||
TangemLogger.withTag(WC_TAG).e("Failed to verify DApp ${sessionProposal.name}", error)
|
||||
CheckDAppResult.FAILED_TO_VERIFY
|
||||
}
|
||||
}
|
||||
else -> CheckDAppResult.FAILED_TO_VERIFY
|
||||
}
|
||||
val requestedNetworks = proposalAccountNetwork
|
||||
.values.map { it.available.plus(it.required) }.flatten().toSet()
|
||||
analytics.send(
|
||||
WcAnalyticEvents.PairRequested(
|
||||
dAppName = sessionProposal.name,
|
||||
dAppUrl = sessionProposal.url,
|
||||
dAppUrl = displayUrl,
|
||||
network = requestedNetworks,
|
||||
domainVerification = verificationInfo,
|
||||
),
|
||||
|
|
@ -221,7 +234,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
|
|||
val appMetaData = WcAppMetaData(
|
||||
name = sessionProposal.name,
|
||||
description = sessionProposal.description,
|
||||
url = sessionProposal.url,
|
||||
url = displayUrl,
|
||||
icons = sessionProposal.icons.map { it.toString() },
|
||||
redirect = sessionProposal.redirect,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import com.reown.walletkit.client.Wallet
|
|||
import com.reown.walletkit.client.WalletKit
|
||||
import com.tangem.domain.walletconnect.WC_TAG
|
||||
import com.tangem.data.walletconnect.utils.WcSdkObserver
|
||||
import com.tangem.data.walletconnect.utils.getDappOriginUrl
|
||||
import com.tangem.datasource.local.walletconnect.WalletConnectStore
|
||||
import com.tangem.domain.walletconnect.model.WcPairError
|
||||
import com.tangem.domain.walletconnect.model.WcPairError.ApprovalFailed
|
||||
|
|
@ -110,9 +109,10 @@ internal class WcPairSdkDelegate(
|
|||
sessionProposal: Wallet.Model.SessionProposal,
|
||||
verifyContext: Wallet.Model.VerifyContext,
|
||||
) {
|
||||
val sessionProposalWithRealUrl = sessionProposal.copy(url = verifyContext.getDappOriginUrl())
|
||||
// Triggered when wallet receives the session proposal sent by a Dapp
|
||||
onSessionProposal.trySend(sessionProposalWithRealUrl to verifyContext)
|
||||
// Triggered when wallet receives the session proposal sent by a Dapp.
|
||||
// Pass the proposal through unchanged so consumers can decide between the dApp-claimed
|
||||
// metadata url (sessionProposal.url) and the Verify-API origin (verifyContext.getDappOriginUrl()).
|
||||
onSessionProposal.trySend(sessionProposal to verifyContext)
|
||||
}
|
||||
|
||||
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {
|
||||
|
|
|
|||
|
|
@ -147,6 +147,25 @@ internal class DefaultWcPairUseCaseTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifyDApp uses verifyContext origin when sessionProposal url is spoofed`() = runTest {
|
||||
val spoofedProposal = sdkProposal.copy(url = "https://evil-spoofed.example/")
|
||||
coEvery { sdkDelegate.pair(url) } returns (spoofedProposal to sdkVerifyContext).right()
|
||||
coEvery { associateNetworksDelegate.associateAccounts(spoofedProposal) } returns mapOf()
|
||||
coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE }
|
||||
|
||||
val useCase = useCaseFactory()
|
||||
useCase.invoke().test {
|
||||
assertEquals(loading, awaitItem())
|
||||
coVerifyOrder {
|
||||
sdkDelegate.pair(url)
|
||||
blockAidVerifier.verifyDApp(DAppData(sdkVerifyContext.origin))
|
||||
}
|
||||
assert(awaitItem() is WcPairState.Proposal)
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `success pair and approve flow`() = runTest {
|
||||
val approveLoading = WcPairState.Approving.Loading(sessionForApprove)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue