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 范围过大
不要将支付会话 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。更可靠的流程如下:
- 父应用通过服务端创建订单。
- 服务端将金额、币种、商户、用户和订单状态保存到数据库。
- 服务端生成一次性且短时有效的支付会话标识。
- iframe 只携带该会话标识访问支付页面。
payments.company.com在服务端查询订单信息。- 卡号从 iframe 直接提交给支付后端或通过 PCI 验证的支付服务商。
- 服务端通过回调确认支付结果。
- 父页面只接收不含持卡人数据的完成通知。
例如:
<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,也不是这些站点同属一个二级域名这一事实。
备注:内容仅供参考。
