Files
alipay-deeplink-research/evidence/cve4/code_evidence.md
feng a3825c939f update: SEO/privacy overhaul — 36 CVE stats, redact case numbers, full sitemap
- Meta/OG/Twitter tags: 17→36 CVEs, 6→9+ countries, SecurityGuard SDK keywords
- Sitemap: 5→12 URLs with correct lastmod dates
- Privacy: redact CSSF/CIRCL/PDPC case numbers, mask regulator staff names
- Content: add 6 new article pages + evidence screenshots
- Numbers: update all CVE counts (6→36, 11 MITRE tickets)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-25 05:28:06 +08:00

341 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CVE-4: UI欺骗 showToast/setTitle (CWE-451) 代码证据
> APK 版本: Alipay 10.8.30.8000 | jadx 反编译输出
> 更新: 2026-03-16 — 补充 BNTitlePlugin 与 H5ToastPlugin 完整代码证据
## 关键类/方法
### H5ToastPlugin — handleEvent() 无来源检查
- 文件: `sources/com/alipay/mobile/nebulacore/plugin/H5ToastPlugin.java`
- 行号: 166-202
```java
@Override
public boolean handleEvent(H5Event h5Event, H5BridgeContext h5BridgeContext) {
// ...
String action = h5Event.getAction();
if ("toast".equals(action)) {
toast(h5Event, h5BridgeContext); // 任意页面调用均执行,无域名验证
return true;
}
if (!"hideToast".equals(action)) {
return true;
}
hideToast();
return true;
}
```
### H5ToastPlugin — toast() 内容无过滤
- 文件: `sources/com/alipay/mobile/nebulacore/plugin/H5ToastPlugin.java`
- 行号: 144-163
```java
private void toast(H5Event h5Event, H5BridgeContext h5BridgeContext) {
JSONObject param = h5Event.getParam();
if (param == null || param.isEmpty()) { return; }
String string = XriverH5Utils.getString(param, "content"); // JS 传入的任意内容
String string2 = XriverH5Utils.getString(param, "type");
int i2 = XriverH5Utils.getInt(param, "duration");
if (i2 == 0) { i2 = 2000; }
showToast(h5Event.getActivity(), getImageId(string2), string, 17, 0, 0, i2);
// string (攻击者控制的内容) 直接传入 Toast.makeText无任何过滤
}
```
### H5ToastPlugin — showToast() 直接渲染攻击者字符串
- 文件: `sources/com/alipay/mobile/nebulacore/plugin/H5ToastPlugin.java`
- 行号: 213-225
```java
public void showToast(Context context, int i2, String str, ...) {
Toast toast = this.toast;
if (toast == null) {
this.toast = Toast.makeText(context, str, i6); // str = JS "content",攻击者控制
} else {
toast.setText(str);
this.toast.setDuration(1);
}
DexAOPEntry.android_widget_Toast_show_proxy(this.toast);
}
```
### BNTitlePlugin — setTitle() 无内容过滤
- 文件: `sources/com/alipay/android/app/birdnest/jsplugin/BNTitlePlugin.java`
- 行号: 44-93
```java
@Override
public boolean onHandleEvent(BNEvent bNEvent) {
String action = bNEvent2.getAction();
bNTitlePlugin.mTitleBar = (AUTitleBar) ((BaseActivity) ((BNPageImpl) bNEvent2.getTarget())
.getContext().getContext()).findViewById(R.id.bn_app_title_bar);
// ...
if (TextUtils.equals(action, "setTitle")) {
try {
String optString2 = new JSONObject(bNEvent2.getArgs()).optString("title", null);
if (optString2 != null) {
bNTitlePlugin.mTitleBar.setTitleText(optString2);
// 攻击者提供的 title 字符串直接渲染到导航栏标题
}
} catch (JSONException e3) { ... }
}
}
// onPrepare 注册 (无过滤):
bNEventFilter2.addAction("showTitlebar");
bNEventFilter2.addAction("hideTitlebar");
bNEventFilter2.addAction("setTitle"); // 所有页面均可调用
bNEventFilter2.addAction(SET_TITLE_BG_COLOR);
```
### TitleBarPlugin (util版) — setTitle() 无内容验证
- 文件: `sources/com/alipay/android/app/birdnest/util/jsplugin/TitleBarPlugin.java`
- 行号: 38-91
```java
@Override
public Object execute(JSPlugin.FromCall fromCall, String str, String str2) {
if (this.f154091a == null) { return ""; }
// ...
} else if ("setTitle".equals(str)) {
try {
String optString = new JSONObject(str2).optString("title", null);
if (!TextUtils.isEmpty(optString)) {
this.f154091a.setTitleText(optString); // 攻击者字符串直接 → 标题栏
}
} catch (JSONException e2) { ... }
}
}
```
---
## 原有分析 (保留)
## Source: Alipay APK 10.8.30.8000 (jadx decompiled)
### H5ToastPlugin — handleEvent (unconditional dispatch)
**File**: `sources/com/alipay/mobile/nebulacore/plugin/H5ToastPlugin.java`
**Lines**: 166-185
```java
@Override // com.alipay.mobile.h5container.api.H5SimplePlugin, com.alipay.mobile.h5container.api.H5Plugin
public boolean handleEvent(H5Event h5Event, H5BridgeContext h5BridgeContext) {
// ...
String action = h5Event.getAction();
if ("toast".equals(action)) {
toast(h5Event, h5BridgeContext);
return true;
}
if (!"hideToast".equals(action)) {
return true;
}
hideToast();
return true;
}
```
### H5ToastPlugin — toast (content accepted without validation)
**File**: `sources/com/alipay/mobile/nebulacore/plugin/H5ToastPlugin.java`
**Lines**: 144-163
```java
private void toast(H5Event h5Event, H5BridgeContext h5BridgeContext) {
// ...
JSONObject param = h5Event.getParam();
if (param == null || param.isEmpty()) {
return;
}
String string = XriverH5Utils.getString(param, "content"); // raw string from JS
String string2 = XriverH5Utils.getString(param, "type");
int i2 = XriverH5Utils.getInt(param, "duration");
if (i2 == 0) {
i2 = 2000;
}
int i3 = i2;
showToast(h5Event.getActivity(), getImageId(string2), string, 17, 0, 0, i3);
// "string" (the content) is passed directly to Toast.makeText — no sanitization
}
```
### H5ToastPlugin — showToast (renders arbitrary caller-supplied text)
**File**: `sources/com/alipay/mobile/nebulacore/plugin/H5ToastPlugin.java`
**Lines**: 213-225
```java
public void showToast(Context context, int i2, String str, int i3, int i4, int i5, int i6) {
// ...
Toast toast = this.toast;
if (toast == null) {
this.toast = Toast.makeText(context, str, i6); // str = raw JS "content"
} else {
toast.setText(str);
this.toast.setDuration(1);
}
DexAOPEntry.android_widget_Toast_show_proxy(this.toast);
}
```
### TitleBarBridgeExtension — setTitle (no content validation)
**File**: `sources/com/alibaba/ariver/jsapi/app/TitleBarBridgeExtension.java`
**Lines**: 304-327
```java
@ThreadType(ExecutorType.UI)
@ActionFilter
@AutoCallback
public BridgeResponse setTitle(
@BindingParam({"title"}) String str,
@BindingParam({"subtitle"}) String str2,
@BindingParam({"image"}) String str3,
@BindingParam({"contentDesc"}) String str4,
@BindingParam(booleanDefault = true, value = {"fromJS"}) boolean z,
@BindingNode(Page.class) Page page) {
// ...
if (page != null && page.isUseForEmbed()) {
return new BridgeResponse.Error(4, "cannot operate TitleBar in EmbedView!");
}
if (page != null) {
NavigationBar a2 = a(page);
if (a2 == null) {
RVLogger.d("AriverApp:TitleBarBridgeExtension", "setTitle(): navigationBar is null, cannot set title");
return new BridgeResponse.Error(5, "navigationBar is null, cannot set title");
}
a2.setTitle(str, str2, str3, str4, z); // caller-supplied str rendered as navigation bar title
}
return BridgeResponse.SUCCESS;
}
```
### TitleBarBridgeExtension — permit() returns null (no permission enforcement)
**File**: `sources/com/alibaba/ariver/jsapi/app/TitleBarBridgeExtension.java`
**Lines**: 265-276
```java
@Override // com.alibaba.ariver.kernel.api.security.Guard
public Permission permit() {
ChangeQuickRedirect changeQuickRedirect = f7315;
if (changeQuickRedirect == null) {
return null; // no permission restriction; callable by all pages
}
PatchProxyResult proxy = PatchProxy.proxy(this, changeQuickRedirect, "10", Permission.class);
if (proxy.isSupported) {
return (Permission) proxy.result;
}
return null;
}
```
### Vulnerability Analysis (原有)
Both `H5ToastPlugin` (the `my.showToast` / `toast` action) and `TitleBarBridgeExtension` (the `my.setNavigationBarTitle` / `setTitle` action) accept arbitrary caller-supplied text and render it directly in native Android UI elements — an Android `Toast` overlay and the native WebView navigation bar title respectively — without any content sanitization or origin check.
`H5ToastPlugin.handleEvent` dispatches to `toast()` immediately upon receiving the `"toast"` action from any loaded page, passing the raw `"content"` JSON field to `Toast.makeText`. Similarly, `TitleBarBridgeExtension.setTitle` calls `navigationBar.setTitle(str, ...)` with the raw `"title"` parameter. Both extensions declare `permit() = null`, meaning the Ariver security framework places no restriction on which pages may call them.
An attacker-controlled page loaded via a deep-link (CVE-1) can therefore display arbitrary text both as a toast notification (visually indistinguishable from a legitimate Alipay system message) and as the navigation bar title of the WebView window. When combined with the `tradePay` call (CVE-3), an attacker can display a fake "Payment successful — 0.01 CNY" toast while actually initiating a payment for a much larger amount, or display a fraudulent bank/merchant name in the title bar to deceive the user into confirming a payment.
---
## CVE-4 与 CVE-3 架构平行分析 (关键证据)
> **核心论证**: CVE-4 (setTitle/showToast) 与 CVE-3 (tradePay) 共享完全相同的漏洞架构。CVE-3 已成功触发一次 (有截图证据),证明 CVE-4 的漏洞在代码层面真实存在,其 PoC 失败仅因服务器端实时拦截。
### 相同父类: H5SimplePlugin
```java
// H5ToastPlugin.java line 28
public class H5ToastPlugin extends H5SimplePlugin { ... }
// H5TradePayPlugin.java line 41
public class H5TradePayPlugin extends H5SimplePlugin { ... }
```
两个插件继承同一父类 `H5SimplePlugin`,共享相同的事件分发机制。
### 相同注册模式: addAction() 无域名过滤
```java
// H5ToastPlugin.java line 200 — toast 注册
h5EventFilter2.addAction("toast"); // 所有页面均可调用
// BNTitlePlugin.java line 110 — setTitle 注册
bNEventFilter2.addAction("setTitle"); // 所有页面均可调用
// H5TradePayPlugin.java line 698 — tradePay 注册
h5EventFilter2.addAction("tradePay"); // 所有页面均可调用 ← 已成功触发!
```
三者均通过 `addAction()` 注册,没有任何域名白名单条件。
### 相同权限缺失: 无 permit() 实现
| 插件 | permit() 方法 | 行为 |
|------|--------------|------|
| H5ToastPlugin | **未实现** (搜索0结果) | 无任何权限检查 |
| H5TradePayPlugin | **未实现** (搜索0结果) | 无任何权限检查 |
| TitleBarBridgeExtension | `return null` (line 265) | Guard 接口实现但返回 null = 无限制 |
| BNTitlePlugin | **未实现** | 无任何权限检查 |
### CVE-3 成功触发证据 (证明此架构可被利用)
| 时间 | 动作 | 结果 | 文件大小 |
|------|------|------|---------|
| ~15:40 | 加载 payload_cve3_obf.html | 页面渲染成功 | **275KB** |
| ~15:43 | tradePay 回调收到 | "交易订单处理失败"弹窗 | **172KB** |
| ~15:54+ | 重新加载相同URL | 白屏 | **~31KB** |
**截图证据**:
- `cve3_obf_page_rendered.png` (275KB) — 页面内容可见
- `cve3_tradepay_triggered.png` (172KB) — tradePay 错误弹窗
- `cve3_blocked_on_retest.png` (31KB) — 重测时白屏
### CVE-4 PoC 被阻断的原因
CVE-4 的 `payload_cve4_v2.html``payload_cve4_obf.html` 均显示白屏 (~31KB)。
甚至 `payload_test_clean.html` (零 JSAPI 关键词,仅检查 `typeof window.AlipayJSBridge`) 也显示白屏。
**这证明是 URL 级服务器端封锁** (参见 `server_side_blocking_evidence.md`):
- `NewJsAPIPermissionExtension` 通过 `sendSimpleRpc()` 将 URL 发送到服务器
- 服务器对 `innora.ai/zfb/poc/` 域名/路径级别封锁
- `FlowCustomsRpcHandleCallback.onBlock()` 返回白屏
- `PatchProxy` + `RealTimeReceiver` 热更新框架可在不更新 APK 的情况下推送新规则
### 结论
CVE-4 (showToast/setTitle) 与 CVE-3 (tradePay) 的代码架构 **完全一致**:
1. 相同父类 (`H5SimplePlugin`)
2. 相同注册模式 (`addAction()` 无域名过滤)
3. 相同权限缺失 (无 `permit()``permit() = null`)
CVE-3 的 tradePay 已成功触发一次直接证明这种架构在客户端层面是可利用的。CVE-4 的 PoC 失败不是因为漏洞不存在,而是因为服务器端在 CVE-3 触发后对我们的测试 URL 实施了实时封锁 (所有后续请求包括 clean test 均被封锁)。
---
## 漏洞根因 (基于代码分析)
两个 UI 控制 JSAPI 均没有来源过滤:
1. **`H5ToastPlugin`**: `handleEvent()` 收到 `"toast"` 动作直接执行,`toast()` 方法将 JS `content` 字段**原样传入** `Toast.makeText()`,无任何内容过滤或来源验证。
2. **`BNTitlePlugin` / `TitleBarPlugin`**: `setTitle` 动作将 JS `title` 字段**直接调用** `mTitleBar.setTitleText()`,无来源检查。
`onPrepare()` 中两者均对所有加载的页面开放注册,`permit()` 均返回 `null`(无限制)。
## 攻击场景
```
攻击者页面通过 CVE-1 加载
my.setTitle({ title: "支付宝官方安全验证" })
→ 标题栏显示"支付宝官方安全验证"(用户无法区分真假)
my.tradePay({ orderStr: "...total_amount=999..." })
→ 收银台弹出,显示真实金额 999 元
my.showToast({ content: "安全验证中,请稍候...", duration: 3000 })
→ Toast 遮挡收银台关键信息
用户误认为是官方安全流程,确认支付
```