- 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>
8.8 KiB
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
@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
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
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
@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
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
@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 容器内的所有页面,没有来源域名白名单过滤。
关键证据:
onPrepare()中addAction("tradePay")无任何域名条件startPaymentWithOrderStr()中来源 URL (h5page.getUrl()) 只放入日志 Map,不做拒绝决策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 伪装)
用户被诱导确认支付