Files
codex-app-server/cloud/chv/checkout_detect.py
T
host b55b49b358 feat(chv): 对齐 Muse P10 支付安全——凭证隔离 + 结账检测 + 每次购买人工审批 + 一次性卡号
- 支付凭证隔离: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 字段)。
2026-11-07 00:09:55 +00:00

144 lines
5.7 KiB
Python
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.
"""结账页检测(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)