From 4d79bde5f7bffc6fb037735ba067e96f21f42044 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Fri, 2 Oct 2026 21:16:56 +0400 Subject: [PATCH] fix(dapp): send only fixed, JSON-quoted error messages to dApp pages The polkadot.js responder forwarded Throwable.message of any rejection error to the page and both responders interpolated the message into JavaScript without escaping. Map polkadot.js rejections to a fixed public message (unknown errors become "Request failed"), give MetamaskError an explicit publicMessage, and quote both with JSONObject.quote so a message can no longer break out of the string literal. --- .../metamask/transport/MetamaskResponder.kt | 9 +++++++-- .../web3/polkadotJs/PolkadotJsResponder.kt | 19 ++++++++++++++++++- .../states/PhishingDetectedPolkadotJsState.kt | 3 ++- .../web3/states/Web3ExtensionStateMachine.kt | 1 + 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/metamask/transport/MetamaskResponder.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/metamask/transport/MetamaskResponder.kt index 709292ae..b966985d 100644 --- a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/metamask/transport/MetamaskResponder.kt +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/metamask/transport/MetamaskResponder.kt @@ -4,8 +4,12 @@ import android.util.Log import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.feature_dapp_impl.web3.webview.WebViewHolder import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId +import org.json.JSONObject -sealed class MetamaskError(val errorCode: Int, message: String) : Throwable(message) { +/** + * [publicMessage] is sent to the dApp page as is, so it must never carry internal error details. + */ +sealed class MetamaskError(val errorCode: Int, val publicMessage: String) : Throwable(publicMessage) { class Rejected : MetamaskError(4001, "Action rejected") @@ -33,7 +37,8 @@ class MetamaskResponder(private val webViewHolder: WebViewHolder) { } fun respondError(messageId: String, error: MetamaskError) { - val js = "window.ethereum.sendRpcError($messageId, ${error.errorCode}, \"${error.message}\")" + val message = JSONObject.quote(error.publicMessage) + val js = "window.ethereum.sendRpcError($messageId, ${error.errorCode}, $message)" evaluateJs(js) } diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/PolkadotJsResponder.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/PolkadotJsResponder.kt index 38ed5396..b9226b66 100644 --- a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/PolkadotJsResponder.kt +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/PolkadotJsResponder.kt @@ -3,7 +3,9 @@ package io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs import android.util.Log import io.novafoundation.nova.common.utils.LOG_TAG import io.novafoundation.nova.feature_dapp_impl.web3.Web3Responder +import io.novafoundation.nova.feature_dapp_impl.web3.states.Web3StateMachineHost import io.novafoundation.nova.feature_dapp_impl.web3.webview.WebViewHolder +import org.json.JSONObject class PolkadotJsResponder( private val webViewHolder: WebViewHolder @@ -35,5 +37,20 @@ class PolkadotJsResponder( private fun successSubscription(id: String, result: String) = "window.walletExtension.onAppSubscription(\"$id\", $result)" - private fun failure(id: String, error: Throwable) = "window.walletExtension.onAppResponse(\"$id\", null, new Error(\"${error.message.orEmpty()}\"))" + private fun failure(id: String, error: Throwable): String { + val message = JSONObject.quote(publicErrorMessage(error)) + + return "window.walletExtension.onAppResponse(\"$id\", null, new Error($message))" + } + + /** + * The page is untrusted, so it only ever receives a fixed message per known rejection reason, + * never the text of an arbitrary internal exception. + */ + private fun publicErrorMessage(error: Throwable): String = when (error) { + Web3StateMachineHost.NotAuthorizedException -> "Rejected by user" + Web3StateMachineHost.SigningFailedException -> "Signing failed" + Web3StateMachineHost.PhishingDetectedException -> "Phishing detected!" + else -> "Request failed" + } } diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/states/PhishingDetectedPolkadotJsState.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/states/PhishingDetectedPolkadotJsState.kt index bd5bcaed..a81c37aa 100644 --- a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/states/PhishingDetectedPolkadotJsState.kt +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/polkadotJs/states/PhishingDetectedPolkadotJsState.kt @@ -2,10 +2,11 @@ package io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.states import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.PolkadotJsTransportRequest import io.novafoundation.nova.feature_dapp_impl.web3.states.PhishingDetectedState +import io.novafoundation.nova.feature_dapp_impl.web3.states.Web3StateMachineHost class PhishingDetectedPolkadotJsState : PhishingDetectedState, PolkadotJsState>(), PolkadotJsState { override fun rejectError(): Throwable { - return IllegalAccessException("Phishing detected!") + return Web3StateMachineHost.PhishingDetectedException } } diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/states/Web3ExtensionStateMachine.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/states/Web3ExtensionStateMachine.kt index bd68f700..05b054cb 100644 --- a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/states/Web3ExtensionStateMachine.kt +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/web3/states/Web3ExtensionStateMachine.kt @@ -42,6 +42,7 @@ interface Web3StateMachineHost { object NotAuthorizedException : Exception("Rejected by user") object SigningFailedException : Exception("Signing failed") + object PhishingDetectedException : Exception("Phishing detected!") val selectedAccount: Flow val currentPageAnalyzed: Flow