PAYATHON 2026

iFrame Hosted Payments 的安全与 cross-site scripting 风险

支付小周

结论

这种架构可以降低信用卡数据泄露的风险,但不能只凭 iframe、不同三级域名和 wildcard certificate,就断定信用卡数据已经完全隔离,或者 PCI DSS 范围仅限于 payments.company.com。

浏览器会将下面两个地址视为不同源:

https://www.company.com
https://payments.company.com

两者的主机名不同。正常情况下,同源策略会阻止父页面读取 iframe 内的 DOM、表单字段和 JavaScript 状态。只要持卡人数据始终在 payments.company.com 的 iframe 中采集,并直接提交给支付系统,父应用通常无法读取卡号。

不过,PCI DSS 的范围取决于哪些系统存储、处理或传输持卡人数据,以及哪些系统能够影响这些过程的安全性,并不单纯按域名划分。父页面可以加载、遮挡、替换或重定向支付 iframe,因此仍可能影响支付流程,未必能完全排除在合规范围之外。最终范围需要由收单机构、QSA 或合规负责人结合具体实现确认。

wildcard certificate 不提供额外隔离

通配符证书解决的是 TLS 身份验证和传输加密问题。例如:

*.company.com

它可以覆盖 www.company.com 和 payments.company.com,但不会使两个站点变成同源,也不会加强 iframe 的 DOM 隔离。

通配符证书还会扩大证书私钥的影响范围。如果多个服务器共享同一把私钥,任何一台服务器失陷,都可能危及其他子域名的 TLS 身份。更稳妥的做法包括:

  • 为支付域名配置独立证书和独立私钥。
  • 不在多台业务服务器之间复制支付域名的私钥。
  • 限制证书签发权限,并监控异常签发。
  • 为 payments.company.com 配置独立的部署、权限和密钥管理。

是否使用 wildcard certificate,与浏览器的 XSS 防护没有直接关系。

主要风险不只是读取 iframe

父页面被攻破

同源策略可以阻止父页面直接读取卡号,但攻击者控制父页面后,仍然可以:

  • 将支付 iframe 替换为仿冒页面。
  • 在 iframe 上覆盖透明表单,诱导用户将卡号输入攻击者控制的元素。
  • 将用户重定向到伪造的支付页面。
  • 篡改金额、订单号或支付完成后的状态。
  • 监听设计不严谨的 postMessage 通信。

所以,父页面无法看到卡号,并不代表它无法影响支付安全。

支付页面自身存在 XSS

如果 payments.company.com 存在反射型、存储型或 DOM XSS,攻击脚本会在支付页面自己的源中运行,并能直接读取卡号。iframe 无法防御其内部的 XSS。

支付应用仍需采用常规安全控制,包括:

  • 根据输出位置进行相应的编码。
  • 避免将未经处理的输入写入 innerHTML。
  • 使用参数化查询。
  • 限制第三方 JavaScript。
  • 部署严格的 Content Security Policy。
  • 监控支付页面脚本及其完整性。

document.domain 破坏隔离

父页面和支付页面都不应使用:

document.domain = "company.com";

旧式代码可能通过这种方式放宽子域之间的隔离,让不同子域被当作同源处理。该机制已经弃用,也会削弱此处依赖的安全边界。

不要将支付会话 Cookie 设置到整个父域:

Set-Cookie: session=...; Domain=company.com

这样做可能导致其他子域收到或覆盖该 Cookie。支付 Cookie 应尽量采用 host-only 形式,不设置 Domain:

Set-Cookie: __Host-payment_session=...; Path=/; Secure; HttpOnly; SameSite=Lax

__Host- 前缀要求 Cookie 使用 Secure、Path=/,且不能包含 Domain。具体使用哪个 SameSite 值,需要结合实际跳转和支付流程测试,不能直接套用。

推荐的支付流程

父应用不应把金额、收款对象等敏感业务参数作为可信数据直接交给 iframe。更可靠的流程如下:

  1. 父应用通过服务端创建订单。
  2. 服务端将金额、币种、商户、用户和订单状态保存到数据库。
  3. 服务端生成一次性且短时有效的支付会话标识。
  4. iframe 只携带该会话标识访问支付页面。
  5. payments.company.com 在服务端查询订单信息。
  6. 卡号从 iframe 直接提交给支付后端或通过 PCI 验证的支付服务商。
  7. 服务端通过回调确认支付结果。
  8. 父页面只接收不含持卡人数据的完成通知。

例如:

<iframe
  src="https://payments.company.com/payment-entry.php?session=opaque_one_time_token"
  title="安全支付"
  allow="payment"
  referrerpolicy="strict-origin-when-cross-origin">
</iframe>

不要传递这类可由浏览器修改的参数:

https://payments.company.com/payment-entry.php?amount=100&paid=true

即使参数带有签名,支付服务器仍应重新校验订单状态、金额、币种、用户身份,以及令牌是否已被使用。

安全使用 postMessage

跨源 iframe 通常需要通知父页面支付已经完成或用户已经取消。此类通知可以通过 postMessage 发送,但必须明确指定并验证来源。

支付页面发送消息:

const allowedParent = "https://www.company.com";

window.parent.postMessage(
  {
    type: "payment.completed",
    paymentSessionId: "opaque-session-id"
  },
  allowedParent
);

父页面接收消息:

window.addEventListener("message", (event) => {
  if (event.origin !== "https://payments.company.com") {
    return;
  }

  if (event.source !== paymentIframe.contentWindow) {
    return;
  }

  const message = event.data;

  if (
    typeof message !== "object" ||
    message === null ||
    message.type !== "payment.completed" ||
    typeof message.paymentSessionId !== "string"
  ) {
    return;
  }

  // 向自己的后端查询最终支付状态。
  // 不应仅凭该浏览器消息把订单标记为已支付。
});

不要使用:

window.parent.postMessage(data, "*");

消息中也不应包含卡号、CVV、完整磁道数据或其他持卡人敏感数据。浏览器消息只能用于界面通知,最终支付结果必须由服务端验证。

限制哪些站点可以嵌入支付页面

payments.company.com 返回响应时,应通过 CSP 明确允许哪些父页面嵌入支付页面:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}';
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self' https://api.payment-provider.example;
  frame-ancestors https://www.company.com https://gifts.company.com https://cars.company.com https://hotels.company.com;
  base-uri 'none';
  form-action 'self' https://processor.example;
  object-src 'none'

其中,{RANDOM_NONCE} 必须由服务器为每次响应安全生成,不能使用固定值。允许加载的脚本、接口和支付服务商域名,应按实际部署情况填写。

frame-ancestors 可以阻止任意外部网站嵌入支付页面,降低点击劫持和仿冒风险。旧版 X-Frame-Options 无法妥善表达多个允许来源,可以作为兼容性补充,但主要策略应使用 CSP。

父页面也应部署 CSP,限制它可以加载的 iframe:

Content-Security-Policy:
  default-src 'self';
  frame-src https://payments.company.com;
  object-src 'none';
  base-uri 'self';

PCI DSS 范围如何判断

如果卡号只出现在支付 iframe 中,父应用从不接收、记录或转发卡号,那么父应用通常不会被视为直接处理持卡人数据。这有助于缩小合规范围,但下列部分仍可能被纳入范围:

  • payments.company.com 的服务器、代码、数据库和管理终端。
  • 部署支付应用所使用的 CI/CD、源代码仓库和运维账号。
  • 能够修改支付页面、支付脚本或相关配置的系统。
  • 与持卡人数据环境连接,或者能够影响其安全性的网络和基础设施。
  • 承载结账页面,并能替换、覆盖或重定向 iframe 的系统。
  • DNS、证书、CDN、WAF、密钥管理和监控系统。
  • 支付页面使用的第三方脚本及其供应链。

如果支付页面完全由经过验证的第三方支付服务商提供,商户可能适用更精简的 PCI 自评模式。如果 payments.company.com 由企业自行开发和运营,仅仅将它嵌入 iframe,并不会自动带来同等程度的范围缩减。

此外,当前示例中的三个地址实际使用的都是 www.company.com,区别只在 URL 路径:

/www.company.com/gifts/store
/www.company.com/cars/store
/www.company.com/hotels/store

按照浏览器的安全模型,它们是同源地址,并非三个不同的三级域名。只有下面这类地址才属于不同子域:

https://gifts.company.com/
https://cars.company.com/
https://hotels.company.com/

这个区别不影响 www.company.com 与 payments.company.com 跨源的结论,但会影响 Cookie、CSP、会话隔离和整体风险评估。

落地时应满足的最低条件

  • 持卡人数据只在支付 iframe 中输入。
  • 父页面、日志、分析工具和错误监控中不出现卡号或 CVV。
  • 支付数据直接提交到支付后端,不经过父应用。
  • 支付域使用独立的 Cookie、证书私钥、部署权限和管理账号。
  • 禁止使用 document.domain 放宽同源限制。
  • postMessage 的发送方和接收方都严格验证 origin、source 和消息结构。
  • 使用 CSP 的 frame-ancestors 限制允许嵌入支付页面的站点。
  • 金额和订单状态完全由服务端校验。
  • 最终支付状态通过服务端回调或服务端查询确认。
  • 对支付页面脚本、响应头和文件变更实施完整性监控。
  • 由 QSA 或收单机构确认最终的 PCI DSS 范围和适用的自评问卷。

合理实施后,该方案可以明显减少接触信用卡数据的应用数量。它的安全边界来自浏览器同源策略、服务端验证、独立权限和持续监控,而不是 wildcard certificate,也不是这些站点同属一个二级域名这一事实。

备注:内容仅供参考。