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.
This commit is contained in:
Satoshi Qazi Muhammed committed 2026-10-02 21:16:56 +04:00
1 parent 77314c8901
commit 4d79bde5f7
4 files changed
+28 -4

No files matched your search

@@ -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)
}
@@ -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"
}
}
@@ -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<PolkadotJsTransportRequest<*>, PolkadotJsState>(), PolkadotJsState {
override fun rejectError(): Throwable {
return IllegalAccessException("Phishing detected!")
return Web3StateMachineHost.PhishingDetectedException
}
}
@@ -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<MetaAccount>
val currentPageAnalyzed: Flow<BrowserPageAnalyzed>