From 28771ec1d9499992f9f9d7974e71c7c03fee940f Mon Sep 17 00:00:00 2001 From: Dimitris Dafnis <68849116+jim-daf@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:52:29 +0200 Subject: [PATCH] fix(web3): sanitize RPC URL before injecting into in-app browser pages (UXSS) Resolves #2686 JsInjectorClient.loadInitJs splices the configured RPC URL into a JavaScript template via String.format and the result is injected into every page that loads in the in-app dApp browser. A malicious dApp could exploit this by calling wallet_addEthereumChain with an RPC URL such as https://rpc.example/x"+alert(document.domain)+" After the user accepts the new chain, the URL is persisted to the network database and used as-is on every subsequent page load, breaking out of the RPC-URL string literal in the template and giving the attacker a stored, universal XSS that runs in the origin of any site visited in the browser (including unrelated dApps and major sites). This change adds a sanitizeRpcUrl helper that: * Rejects empty/null URLs. * Rejects URLs containing any character with meaning inside a JS string literal (quotes, backslash, angle brackets, control characters). * Parses the URL with HttpUrl.parse and rejects anything that is not a valid http(s) URL. * Returns the canonical normalized URL string and re-checks it for unsafe characters before returning. setChainId and setTSChainId now run the resolved RPC URL through this helper before storing it. If the URL is unsafe, an empty string is used, which makes the injected provider non-functional for that chain but prevents the UXSS payload from executing. Defensive: loadInitJs also coalesces a null rpcUrl to an empty string so String.format never sees null after the change. --- .../app/web3/JsInjectorClient.java | 59 ++++++++++++++++++- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/alphawallet/app/web3/JsInjectorClient.java b/app/src/main/java/com/alphawallet/app/web3/JsInjectorClient.java index 64f28cca96..7f1f9384f6 100644 --- a/app/src/main/java/com/alphawallet/app/web3/JsInjectorClient.java +++ b/app/src/main/java/com/alphawallet/app/web3/JsInjectorClient.java @@ -30,12 +30,21 @@ public class JsInjectorClient { + private static final String TAG = "JsInjectorClient"; private static final String DEFAULT_CHARSET = "utf-8"; private static final String DEFAULT_MIME_TYPE = "text/html"; private final static String JS_TAG_TEMPLATE = ""; final String SCRIPT_TAG = "\\r\\n\\t\\u0000-\\u001f]"); + private long chainId; private Address walletAddress; @@ -60,14 +69,58 @@ public long getChainId() { public void setChainId(long chainId) { this.chainId = chainId; - this.rpcUrl = EthereumNetworkRepository.getDefaultNodeURL(chainId); + this.rpcUrl = sanitizeRpcUrl(EthereumNetworkRepository.getDefaultNodeURL(chainId)); } // Set ChainId for TokenScript inject public void setTSChainId(long chainId) { this.chainId = chainId; - this.rpcUrl = EthereumNetworkRepository.getDefaultNodeURL(chainId); + this.rpcUrl = sanitizeRpcUrl(EthereumNetworkRepository.getDefaultNodeURL(chainId)); + } + + /** + * Validate and normalize an RPC URL before it is spliced into the + * JavaScript that is injected into every loaded page. Returns an empty + * string when the URL is missing, unparseable, not http(s), or contains + * any character that has meaning inside a JS string literal. This blocks + * the UXSS attack described in issue #2686, where a malicious chain + * registered via {@code wallet_addEthereumChain} could break out of the + * RPC-URL string in the template and inject arbitrary JavaScript. + */ + static String sanitizeRpcUrl(String url) + { + if (TextUtils.isEmpty(url)) + { + return ""; + } + if (UNSAFE_URL_CHARS.matcher(url).find()) + { + Log.w(TAG, "Rejecting RPC URL containing characters unsafe for JS string injection"); + return ""; + } + HttpUrl parsed = HttpUrl.parse(url); + if (parsed == null) + { + Log.w(TAG, "Rejecting unparseable RPC URL"); + return ""; + } + String scheme = parsed.scheme(); + if (!"http".equals(scheme) && !"https".equals(scheme)) + { + Log.w(TAG, "Rejecting RPC URL with non-http(s) scheme: " + scheme); + return ""; + } + // Re-serialize through HttpUrl so any odd encodings are normalized to a + // canonical form before injection. Re-check the normalized form for + // unsafe characters in case parsing decoded any. + String normalized = parsed.toString(); + if (UNSAFE_URL_CHARS.matcher(normalized).find()) + { + Log.w(TAG, "Rejecting normalized RPC URL containing unsafe characters"); + return ""; + } + return normalized; } public String initJs(Context context) @@ -193,7 +246,7 @@ private Request buildRequest(String url, Map headers) { private String loadInitJs(Context context) { String initSrc = loadFile(context, R.raw.init); String address = walletAddress == null ? Address.EMPTY.toString() : Keys.toChecksumAddress(walletAddress.toString()); - return String.format(initSrc, address, rpcUrl, chainId); + return String.format(initSrc, address, rpcUrl == null ? "" : rpcUrl, chainId); } String injectStyleAndWrap(String view, String style)