Android JSBridge 漏洞分析笔记

j1ya Lv5

基础概念

JSBridge:APP在Webview中给H5页面提供调用Java/Kotlin APP原生代码的桥,例如获取登录态,打开相机/相册,定位/分享/支付等等

Deeplink:通过URI将用户引导到特定页面的链接,决定“打开谁”,示例:myapp://open/mywebview?url=https://www.baidu.com

Webview:APP内嵌的网页渲染组件,可以加载H5页面,决定“怎么展示”

典型实现

可以通过 addJavascriptInterface() 将 Java 对象暴露给网页 JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AppBridge {
@JavascriptInterface
public String getAppVersion() {
return "1.0.0";
}
}

webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(
new AppBridge(), "AndroidBridge"
);
webView.loadUrl("https://example.com");
const version = window.AndroidBridge.getAppVersion();
console.log(version);

通过 Deeplink 触发的一种典型攻击流程:

  1. 用户点击来自浏览器,其他APP,扫描二维码得到的恶意链接
  2. 系统根据Intent及 Intent Filter 等信息匹配APP特定Activity
  3. APP解析传入 URI 的scheme,host,path,query等组成部分
  4. 路由模块匹配到WebView加载外部URL
  5. APP 的 WebView 加载了攻击者可控或存在 XSS 的网页
  6. 如果该页面能够访问具有敏感能力的 JSBridge,攻击者可调用相应方法
  7. 根据接口功能和权限校验情况,可能造成敏感信息泄露或未经授权的业务操作

概括:攻击者使可控 JavaScript 在具有桥接能力的 WebView 中执行,由于能调用校验不完善的原生接口,导致用户敏感信息泄露或未经授权的操作

常见场景

1. 任意 URL 加载 + JSBridge 暴露

攻击者通过 Deeplink 控制 WebView URL,使不可信页面获得原生接口调用能力

2. XSS + JSBridge

可信 H5 页面存在 XSS,攻击者注入的 JavaScript 继承该页面可用的桥接能力

3. iframe + JSBridge

addJavascriptInterface() 注册的对象会暴露给包括跨域 iframe,攻击者可控 iframe 中的 JavaScript 因而调用原生接口

真实案例

CVE-2023-41898 in Home Assistant Companion

Home Assistant Companion 是用于连接 Home Assistant 智能家居服务的移动 APP

该 Activity 配置了 android:exported=”true”,并注册了处理 https://my.home-assistant.io/redirect/ 的 Intent Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
<activity android:name=".launch.my.MyActivity"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="https"
android:host="my.home-assistant.io"
android:pathPrefix="/redirect/"/>
</intent-filter>
</activity>

MyActivity.kt 从传入的 Intent 中读取 URL,并在启用 JavaScript 的 WebView 中加载。shouldOverrideUrlLoading() 会识别以 homeassistant://navigate/ 开头的 URL,并将后面的内容传递给 WebViewActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
if (Intent.ACTION_VIEW == intent?.action &&
intent.data != null) {

// ...

binding.webview.apply {
settings.javaScriptEnabled = true
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
val url = request?.url.toString()

if (url.startsWith(
"homeassistant://navigate/"
)) {
startActivity(
WebViewActivity.newInstance(
context,
url.removePrefix(
"homeassistant://navigate/"
)
)
)
finish()
return true
}

return false
}
}
}

binding.webview.loadUrl(newUri.toString())
}

WebViewPresenterImpl.onViewReady() 会将传入的 path 交给 UrlUtil.handle() 处理,该工具函数在识别到绝对 HTTP 或 HTTPS URL 时可以直接将输入转换为 URL

1
2
3
4
5
6
7
8
9
10
11
12
fun handle(base: URL?, input: String): URL? {
return when {
isAbsoluteUrl(input) -> {
URL(input)
}
// --snip--
}
}

fun isAbsoluteUrl(it: String?): Boolean {
return Regex("^https?://").containsMatchIn(it.toString())
}

在 WebViewActivity.kt 中,APP 还注册了 JavaScript Interface,并提供 getExternalAuth() 等方法,getExternalAuth() 接收 JSON 参数,其中 callback 由 JavaScript 提供,并被传递给 Native 端的认证处理逻辑,Native 代码将这个回调字符串拼接到 JavaScript 代码中,再交由 evaluateJavascript() 执行

1
2
3
4
5
6
7
8
9
10
11
@JavascriptInterface
fun getExternalAuth(payload: String) {
JSONObject(payload).let {
presenter.onGetExternalAuth(
this@WebViewActivity,
it.getString("callback"),
it.has("force") &&
it.getBoolean("force")
)
}
}

CVE-2026-78997 in UC Browser

UC 关联域名 mtmsg.uc.cn 的页面会将 email 参数未经充分转义地写入 HTML,存在XSS漏洞

http://mtmsg.uc.cn/ads_rlink.php?email="%3E%3Cscript%3Ealert(1)%3C/script%3E&action=add

该域名又处于 UC Browser 的白名单范围内,因此注入的 JavaScript 可以访问 ucapi.invoke 等桥接能力

UC Browser 向网页提供内部 JavaScript Bridge

1
2
3
4
ucapi.invoke("account.openLoginWindow", {
loginCallback: callback,
dismissCallback: callback
});

其中 account.openLoginWindow 用于打开浏览器的原生登录窗口。loginCallback 和 dismissCallback 是由网页提供的 JavaScript 回调字符串。通过反编译发现,Native 端会把这些回调保存到单例对象中,并关联触发调用的 windowID。回调字符串可以在页面导航后继续存在,但没有将回调与注册时的页面来源可靠绑定,导致页面导航后,原先注册的回调仍被保留,并可能在新页面的上下文中执行

1
2
3
4
// k50/h.java — account.openLoginWindow handler
aVar.f70926n = i; // caller's windowID
aVar.f70927u = jSONObject.optString("loginCallback"); // raw JS string
aVar.v = jSONObject.optString("dismissCallback"); // raw JS string

回调分发逻辑会把 URL 校验字段设置为空字符串

1
2
3
4
5
6
7
8
9
10
11
// m50/a.java — c() — callback dispatcher
public static void c(int i, String str) {
if (TextUtils.isEmpty(str)) return;
StringBuilder sbY = defpackage.e.y("javascript:", str, ";");
HashMap map = new HashMap();
map.put("js", sbY.toString());
map.put("url", ""); // ← EMPTY URL GUARD
map.put("windowID", Integer.valueOf(i));
// sends message 1401
MessagePackerController.getInstance().sendMessage(messageObtain);
}

执行器遇到空校验值时,会直接通过 evaluateJavascript() 执行脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// com/uc/browser/webwindow/i.java — u3()
public final void u3(int i, String str, String str2) {
if (pk0.b.f(str)) return; // null/whitespace check only
WebWindow webWindow = i == -1 ? O2() : b3(i); // find tab by windowID
if (webWindow == null) return;
of0.s sVar = webWindow.K; // get the WebView
if (sVar == null) return;
if (str.startsWith("javascript:")) {
str = str.substring(11); // strip "javascript:" prefix
}
if (TextUtils.isEmpty(str2)) {
sVar.evaluateJavascript(str, null); // ← EXECUTES UNCONDITIONALLY
} else if (str2.equals(sVar.getUrl())) {
sVar.evaluateJavascript(str, null); // only if URL matches
}
}

当用户关闭原生登录窗口时,之前注册的回调可以在当前加载的另一个网站上下文中执行

CVE-2026-44698 in Home Assistant Companion

Android Native 端通过 addJavascriptInterface() 注册桥接对象:window.externalApp

其中包括用于获取外部认证信息的 getExternalAuth,以及用于撤销认证的 revokeExternalAuth,

这使前端能够通过 JavaScript 向 Native 发送请求

Android 的 addJavascriptInterface() 会将注册对象暴露给 WebView 中的所有 frame,包括跨域 iframe。

因此如果 Dashboard 中嵌入了第三方网页,该 iframe 自身的 window 中也会暴露 externalApp 对象

Native 处理 getExternalAuth 时,会将 JSON 中的 callback 字段直接插入 JavaScript 字符串,再通过 evaluateJavascript() 执行

由于没有严格限制回调标识符,也没有充分转义,攻击者可以让原本作为回调名称的数据改变 JavaScript 代码的执行逻辑

而 evaluateJavascript() 在主 frame 的 JavaScript 上下文中执行,因此跨域 iframe 可能借此在 Home Assistant 前端的来源下执行代码

修复建议

  1. 严格解析和校验待加载 URL 的协议、主机,对后续导航和重定向同样设置限制
  2. 只暴露必要能力,敏感操作独立执行权限校验
  3. 及时修复白名单域名的 XSS
  4. 对需要区分来源和主 frame 的桥接,考虑使用 WebViewCompat.addWebMessageListener(),设置严格的 allowedOriginRules,并利用回调中的 sourceOrigin、isMainFrame 实施限制;继续使用 addJavascriptInterface() 时,将第三方内容移至不注册敏感接口的独立 WebView
  5. 避免把网页提供的回调字符串直接拼接为可执行 JavaScript ,并在页面导航后使旧页面注册的待执行回调失效

参考链接:

https://securitylab.github.com/advisories/GHSL-2023-142_Home_Assistant_Companion_for_Android

https://gist.github.com/OmriInbar-Novee/9fd65fe08c1b1cff6a19350e44425de2

https://github.com/home-assistant/core/security/advisories/GHSA-7jp2-p2fw-mgvf

https://developer.android.com/develop/ui/views/layout/webapps/native-api-access-jsbridge

  • 标题: Android JSBridge 漏洞分析笔记
  • 作者: j1ya
  • 创建于 : 2026-10-17 22:23:43
  • 更新于 : 2026-10-17 22:27:08
  • 链接: https://redefine.ohevan.com/2026/10/17/Android-JSBridge-漏洞分析笔记/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论