- 支付凭证隔离:PaymentCredentialVault(只活内存、白名单
stripe-link/shop-pay、只存不透明 id + 掩码标签、原始卡号
PAN 拒绝入库、按次释放绑定商家/金额/货币、单次 + TTL)
- 结账页检测:detect_checkout(URL + 无障碍树信号,确定性
规则、不读原始 DOM、与 P4 detect_form_risk 同输入形状)
- 每次购买人工审批:PaymentGate 经 P5 对话外通道发审批,
payload 含确切购买详情;完成时先验签、再验详情摘要
(篡改即拒且审批保持待处理)、最后单次消费答复
- 一次性卡号:OneTimeCard 绑定商家/金额/货币/有效期,单次
兑现;SimulatedWalletBackend 沙箱钱包(假 PAN 只在后端
私有内存、charge 只记账不联网、取消即 void)
- 限额:SpendLimits 三档 minor units 比对,限额不可用时
不编造上限(limits_checked=False)
- 审计:PaymentAudit 只留允许字段,秘密字段名丢弃 + redacted
- 审批通道最小扩展:ApprovalRequest 增加可选 details 字段,
不传时 payload 与 P5 完全一致
- 真实支付与扣款不做:全部沙箱验证,真实钱包集成待用户
另行批准;新测试 79 项,全量 cloud 套件 2037 OK
round-12 集成说明:本提交为 acf98f8 的 cherry-pick(只含 P10
自有 diff,基线 25bcd35 已含的 P3/P5 内容未重复合入;
approval_channel.py 仅加可选 details 字段)。
144 lines
5.7 KiB
Python
144 lines
5.7 KiB
Python
"""结账页检测(feat-chv-muse-p10,叶子模块,纯标准库)。
|
||
|
||
对齐 Muse 官方安全文(security.muse.ai 2026-09-08,见
|
||
docs/chv-muse-p10-gap.md):已保存支付凭证的网站,浏览器内置
|
||
结账页检测,每次都弹出人工审批并展示确切购买详情。
|
||
|
||
本模块是纯函数,不启动浏览器、不联网、不读原始 DOM:
|
||
- 输入只有 URL + 无障碍树节点(与 P4 的 ``detect_form_risk``
|
||
同输入形状——P2 的浏览器子代理只返回无障碍树快照,节点
|
||
只有 role/name/label/type 等描述字段);
|
||
- 输出 ``CheckoutVerdict``:是否结账页、置信度、命中原因、
|
||
表单风险、页面金额文本;
|
||
- 确定性规则,不冒充模型分类器(见 P6 口径)。
|
||
|
||
诚实边界:启发式检测,不是 100% 检出——商户自定义结账页、
|
||
单页应用内嵌结账可能漏检。漏检时的安全底线在调用方
|
||
(``PaymentGate`` 拒绝任何未经审批的扣款),不在本模块
|
||
宣称“所有结账页都会被拦下”。
|
||
|
||
页面金额文本(``amount_text``)只作展示信号:扣款金额永远
|
||
以 ``PurchaseDetails``(订单数据)为准,页面文本是不受信任
|
||
输入(P6 口径),不得直接拿去扣款。
|
||
"""
|
||
|
||
import re
|
||
from dataclasses import dataclass, field
|
||
from urllib.parse import urlparse
|
||
|
||
# URL 路径段信号:结账步骤(高)与购物车(中,结账前一步)。
|
||
_CHECKOUT_SEGMENTS = ("checkout", "check-out", "payment", "pay",
|
||
"billing", "order-review", "place-order",
|
||
"confirm-order")
|
||
_CART_SEGMENTS = ("cart", "basket", "bag")
|
||
|
||
# 无障碍树节点文本信号(小写子串匹配)。
|
||
_PAYMENT_HINTS = ("card number", "credit card", "debit card",
|
||
"cardholder", "expiry", "expiration", "exp date",
|
||
"cvc", "cvv", "security code", "卡号", "信用卡",
|
||
"有效期", "安全码")
|
||
_ORDER_HINTS = ("place order", "pay now", "confirm purchase",
|
||
"complete purchase", "checkout", "下单", "立即支付",
|
||
"确认购买", "去支付", "提交订单")
|
||
_PASSWORD_HINTS = ("password", "密码")
|
||
|
||
_AMOUNT_RE = re.compile(
|
||
r"[$€£¥]\s?\d[\d,]*(?:\.\d{1,2})?"
|
||
r"|\b\d[\d,]*(?:\.\d{1,2})?\s?(?:USD|EUR|GBP|JPY|CNY)\b")
|
||
|
||
|
||
@dataclass
|
||
class CheckoutVerdict:
|
||
"""一次结账检测的结果(只含描述,不含任何凭证值)。"""
|
||
|
||
is_checkout: bool
|
||
confidence: str # high / medium / low
|
||
reasons: list = field(default_factory=list)
|
||
form_risk: str | None = None # payment / password / None
|
||
amount_text: str | None = None
|
||
|
||
|
||
def _node_text(node: dict) -> str:
|
||
"""一个无障碍树节点的描述文本(role/name/label/type
|
||
拼起来,小写;原始 DOM 从不到这里)。"""
|
||
parts = [str(node.get(k, "")) for k in
|
||
("role", "name", "label", "type")]
|
||
return " ".join(parts).lower()
|
||
|
||
|
||
def _url_level(url: str) -> str:
|
||
"""URL 信号档位:high(结账路径)/ medium(购物车)/
|
||
none。只看 path,不看 query(query 里可能有订单号等
|
||
敏感值,不进原因)。"""
|
||
try:
|
||
path = urlparse(url).path.lower()
|
||
except ValueError:
|
||
return "none"
|
||
segments = [s for s in path.split("/") if s]
|
||
if any(s in _CHECKOUT_SEGMENTS for s in segments):
|
||
return "high"
|
||
if any(s in _CART_SEGMENTS for s in segments):
|
||
return "medium"
|
||
return "none"
|
||
|
||
|
||
def _node_signals(nodes: list) -> tuple:
|
||
"""无障碍树信号:(支付字段, 密码字段, 下单按钮, 金额文本)。
|
||
节点文本只作特征匹配,不存原文。"""
|
||
payment = False
|
||
password = False
|
||
order = False
|
||
amount: str | None = None
|
||
for node in nodes or []:
|
||
text = _node_text(node)
|
||
if not text.strip():
|
||
continue
|
||
if any(h in text for h in _PAYMENT_HINTS):
|
||
payment = True
|
||
if any(h in text for h in _PASSWORD_HINTS):
|
||
password = True
|
||
if any(h in text for h in _ORDER_HINTS):
|
||
order = True
|
||
if amount is None:
|
||
hit = _AMOUNT_RE.search(text)
|
||
if hit:
|
||
amount = hit.group(0)
|
||
return payment, password, order, amount
|
||
|
||
|
||
def detect_checkout(url: str, nodes: list | None = None
|
||
) -> CheckoutVerdict:
|
||
"""检测一个页面是否为结账页。
|
||
|
||
判定(确定性规则):支付字段在页 → 结账页(高置信);
|
||
结账路径 URL → 结账页(有下单按钮/金额则高,否则中);
|
||
下单按钮 + 金额文本 → 结账页(中置信);只命中购物车
|
||
路径 → 不是结账页(购物车是结账前一步);其余 → 不是。
|
||
``form_risk``:支付字段在页为 ``payment``,否则密码字
|
||
段在页为 ``password``,与 P4 的 ``detect_form_risk``
|
||
口径一致。"""
|
||
level = _url_level(url)
|
||
has_payment, has_password, has_order, amount = _node_signals(
|
||
nodes or [])
|
||
reasons: list = []
|
||
if level == "high":
|
||
reasons.append("checkout_url")
|
||
elif level == "medium":
|
||
reasons.append("cart_url")
|
||
if has_payment:
|
||
reasons.append("payment_fields")
|
||
if has_order:
|
||
reasons.append("order_button")
|
||
if amount is not None:
|
||
reasons.append("amount_text")
|
||
risk = "payment" if has_payment else (
|
||
"password" if has_password else None)
|
||
if has_payment:
|
||
return CheckoutVerdict(True, "high", reasons, risk, amount)
|
||
if level == "high":
|
||
conf = "high" if has_order or amount else "medium"
|
||
return CheckoutVerdict(True, conf, reasons, risk, amount)
|
||
if has_order and amount is not None:
|
||
return CheckoutVerdict(True, "medium", reasons, risk, amount)
|
||
return CheckoutVerdict(False, "low", reasons, risk, amount)
|