Files
alipay-deeplink-research/evidence/cve3/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

208 lines
8.8 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-3: tradePay未授权调用 (CWE-940) 代码证据
> APK 版本: Alipay 10.8.30.8000 | jadx 反编译输出
> 更新: 2026-03-16 — 补充 H5TradePayPlugin 代码证据
## 关键类/方法
### H5TradePayPlugin — onPrepare() JSAPI 注册
- 文件: `sources/com/alipay/mobile/framework/service/ext/phonecashier/H5TradePayPlugin.java`
- 行号: 686-701
```java
@Override
public void onPrepare(H5EventFilter h5EventFilter) {
// ...
h5EventFilter2.addAction("tradePay"); // 注册给所有 WebView 页面,无域名过滤
h5EventFilter2.addAction("deposit");
h5EventFilter2.addAction(TRADE_URL); // "tradeUrl"
}
```
### H5TradePayPlugin — startPaymentWithOrderStr() 来源域名仅用于日志
- 文件: `sources/com/alipay/mobile/framework/service/ext/phonecashier/H5TradePayPlugin.java`
- 行号: 522-603
```java
public boolean a(String str, a aVar, H5Event h5Event, String str2, Map<String, String> map) {
// ...
if (h5Page != null) {
Bundle params = h5Page.getParams();
String string = H5Utils.getString(params, "appId");
boolean z2 = H5Utils.getBoolean(params, "isTinyApp", false);
// ...
if (TextUtils.equals(str2, "tradePay")) {
z = true;
if (z2) { // 来自小程序
str4 = H5PayUtil.generateTinybizContext4OrderStr(str4, string, str3);
hashMap.put("invoke_from_source", "tinyapp");
hashMap.put("invoke_from_id", string);
hashMap.put("invoke_from_api", "tradepay");
} else { // 来自 H5 页面
str4 = H5PayUtil.generateH5bizContext4OrderStr(str4, h5Page.getUrl());
hashMap.put("invoke_from_source", "h5page");
hashMap.put("invoke_from_api", "tradepay");
String realRefer = H5Utils.getRealRefer(h5Page, h5Page.getUrl());
// ... realRefer 被截断到 30 字符,只放入日志 map不做校验
hashMap.put("invokeFromReferUrl", realRefer); // 仅日志,非访问控制
}
// ...
phoneCashierServcie.boot(str4, a(aVar, null, null), hashMap);
// ^ 直接启动收银台,来源 URL 只进日志,不拒绝非白名单调用方
}
}
}
```
### H5TradePayPlugin — 常量定义
- 文件: `sources/com/alipay/mobile/framework/service/ext/phonecashier/H5TradePayPlugin.java`
- 行号: 42-48
```java
public static final String APPID = "appid";
public static final String APPID_CONTENT = "alipay";
public static final String DEPOSIT = "deposit";
public static final String SYSTEM = "system";
public static final String SYSTEM_CONTENT = "android";
public static final String TAG = "H5TradePayPlugin";
public static final String TRADE_PAY = "tradePay"; // JSAPI 名称
public static final String TRADE_URL = "tradeUrl";
```
---
## 原有分析 (保留)
## Source: Alipay APK 10.8.30.8000 (jadx decompiled)
### TradePayBridgeExtension — tradePay (annotated entry point)
**File**: `sources/com/alipay/mobile/phonecashier/TradePayBridgeExtension.java`
**Lines**: 270-287
```java
@NativeActionFilter
@Remote
public void tradePay(@BindingApiContext ApiContext apiContext, @BindingRequest JSONObject jSONObject,
@BindingCallback BridgeCallback bridgeCallback) {
// ...
if (jSONObject == null) {
handleException(bridgeCallback);
return;
}
if (apiContext instanceof ExtHubApiContext) {
this.mBizType = ((ExtHubApiContext) apiContext).getBizType();
this.mAppId = apiContext.getAppId(); // records caller appId for logging only
}
this.mBizContext = jSONObject.getString(LONG_SAFEPAY_CONTEXT);
this.needEraseMemo = !TextUtils.equals(
PhoneCashierMspEngine.hn().getWalletConfig("MQP_degrade_tradepay_erase_memo_10556"),
"10000");
tradePay(bridgeCallback, jSONObject); // proceeds directly to payment boot
}
```
### TradePayBridgeExtension — tradePay (payment boot, no origin validation)
**File**: `sources/com/alipay/mobile/phonecashier/TradePayBridgeExtension.java`
**Lines**: 219-268
```java
public void tradePay(BridgeCallback bridgeCallback, JSONObject jSONObject) {
// ...
PhoneCashierServcie phoneCashierServcie = (PhoneCashierServcie)
LauncherApplicationAgent.getInstance()
.getMicroApplicationContext()
.findServiceByInterface(PhoneCashierServcie.class.getName());
if (phoneCashierServcie == null) {
LogUtil.record(1, TAG, "cashierService is null.");
handleException(bridgeCallback);
return;
}
String string = jSONObject.getString("bizContext");
if (TextUtils.isEmpty(string)) {
string = this.mBizContext;
}
if (jSONObject.containsKey(ApLinkTokenUtils.ORDER_STRING_SPM_EXT_KEY)) {
this.mOrderInfo = jSONObject.getString(ApLinkTokenUtils.ORDER_STRING_SPM_EXT_KEY);
// appends bizcontext to orderInfo string, then boots cashier
if (!TextUtils.isEmpty(string) && !TextUtils.isEmpty(this.mOrderInfo)
&& !this.mOrderInfo.contains("&bizcontext=")) {
this.mOrderInfo += "&bizcontext=\"" + string + "\"";
}
HashMap hashMap = new HashMap();
addExtendInfo(jSONObject, hashMap);
phoneCashierServcie.boot(this.mOrderInfo, getPayCallback(bridgeCallback), hashMap);
// ... logging only, no origin check before this call
return;
}
if (jSONObject.containsKey("tradeNO")) {
this.mTradeNo = jSONObject.getString("tradeNO");
String string2 = jSONObject.getString("bizType");
if (TextUtils.isEmpty(string2)) {
string2 = "trade";
}
PhoneCashierOrderExp phoneCashierOrderExp = new PhoneCashierOrderExp();
phoneCashierOrderExp.setBizType(string2);
phoneCashierOrderExp.setOrderNo(this.mTradeNo);
// ...
phoneCashierServcie.boot(phoneCashierOrderExp, payCallback, hashMap3);
// boots cashier with caller-supplied tradeNO, no origin validation
}
}
```
### TradePayBridgeExtension — permit() returns null
**File**: `sources/com/alipay/mobile/phonecashier/TradePayBridgeExtension.java`
**Lines**: 206-217
```java
@Override // com.alibaba.ariver.kernel.api.security.Guard
public Permission permit() {
ChangeQuickRedirect changeQuickRedirect = f83420;
if (changeQuickRedirect == null) {
return null; // <-- no permission declared; framework allows all callers
}
PatchProxyResult proxy = PatchProxy.proxy(this, changeQuickRedirect, "12", Permission.class);
if (proxy.isSupported) {
return (Permission) proxy.result;
}
return null;
}
```
### Vulnerability Analysis (原有)
`TradePayBridgeExtension` implements the `tradePay` JSBridge API exposed to every WebView page running inside Alipay. The annotated entry point extracts `appId` and `bizType` from the caller context but uses them only for logging (via `addEventLog`), never as an access-control decision. The critical security guard point is `permit()`, which unconditionally returns `null` — the Ariver framework interprets a null `Permission` as "no restriction", meaning the API is callable from any page regardless of origin.
When `phoneCashierServcie.boot()` is called it opens the native payment cashier UI with the caller-supplied `orderInfo` string or `tradeNO`. An attacker who loads a malicious page via a deep-link (CVE-1) can therefore invoke `tradePay` with a crafted order string, launching the payment UI for an attacker-controlled transaction. While the user still sees a confirmation UI before funds are debited, the attacker controls the displayed price and recipient, enabling social-engineering / UI-spoofing fraud when combined with CVE-4.
---
## 漏洞根因 (基于代码分析)
`H5TradePayPlugin``TradePayBridgeExtension` 均将 `tradePay` JSAPI 注册给支付宝 H5 容器内的**所有**页面,没有来源域名白名单过滤。
关键证据:
1. `onPrepare()``addAction("tradePay")` 无任何域名条件
2. `startPaymentWithOrderStr()` 中来源 URL (`h5page.getUrl()`) 只放入日志 Map不做拒绝决策
3. `permit()` 返回 `null`,框架解释为"无限制"
攻击者通过 CVE-1 将页面加载进支付宝 WebView 后,可立即调用 `my.tradePay({ orderStr: ... })` 触发支付界面,用户看到的收款方/金额均由攻击者的 `orderStr` 控制。
## 攻击路径
```
通过 CVE-1 加载攻击者页面到支付宝 WebView
my.tradePay({ orderStr: "out_trade_no=FAKE&total_amount=9999&..." })
H5TradePayPlugin.interceptEvent() / handleEvent()
startPaymentWithOrderStr() — 来源 URL 只记日志,不拒绝
phoneCashierServcie.boot(orderStr, callback, extInfo)
收银台 UI 弹出,显示攻击者控制的金额和收款方
↓ (结合 CVE-4 的 setTitle/showToast 伪装)
用户被诱导确认支付
```