通过 iframe 嵌入第三方支付表单有哪些安全风险?
明确结论
即使父页面和支付 iframe 都使用 HTTPS,整个支付流程也不一定安全。
HTTPS 只能保证传输过程的机密性和完整性,无法证明承载 iframe 的网站是善意的。虽然父页面通常不能直接读取跨域 iframe 中的银行卡号,但仍然可以:
- 替换、移动、遮挡或伪装 iframe;
- 通过透明覆盖层实施点击劫持或界面欺骗;
- 向用户展示虚假的商户、金额或支付结果;
- 导航 iframe,或直接用仿冒表单替换它;
- 向 iframe 发送伪造的
postMessage消息; - 篡改订单创建请求中的金额、币种、商品和商户标识。
如果父页面使用 HTTP,风险会高得多。网络攻击者可以修改整个父页面,而不只是 iframe 的 location。因此,支付表单应只允许嵌入 HTTPS 页面,并结合服务端校验、短期签名令牌、域名白名单、严格的消息验证和支付数据隔离,建立完整的信任边界。
跨域 iframe 能保护什么
假设商户页面位于:
https://merchant.example
支付 iframe 位于:
https://payments.example
两者的 origin 不同,浏览器的同源策略通常会阻止父页面直接访问 iframe 的 DOM。例如,父页面无法正常执行以下代码来读取卡号:
const frame = document.querySelector("iframe");
const cardNumber = frame.contentWindow.document
.querySelector("#card-number")
.value;
浏览器会拒绝这类跨域访问。
只要银行卡输入框确实位于支付服务商控制的跨域 iframe 中,并且支付服务本身没有 XSS 等漏洞,商户页面通常就无法直接读取输入内容。
不过,同源策略只隔离文档内容,不会让 iframe 脱离父页面的控制。父页面仍然可以控制 iframe 元素的位置、大小、可见性和 src。
HTTPS 父页面仍然可能是恶意的
界面伪装与点击劫持
父页面可以在 iframe 上方或周围放置自己的元素。它可以隐藏真实的支付金额,在 iframe 上叠加误导性说明,也可以利用透明布局诱导用户点击 iframe 中的确认按钮。
.payment-frame {
position: absolute;
inset: 0;
opacity: 0.01;
z-index: 10;
}
.fake-interface {
position: relative;
z-index: 1;
}
这类攻击通常称为 UI redressing,其中包括点击劫持。它是否会造成实际损失,取决于支付表单的交互方式。如果最终付款只需要点击一次,而 iframe 内又没有清楚显示收款方、金额和确认动作,风险就会明显增加。
支付 iframe 内至少应独立显示以下信息:
- 收款商户;
- 支付金额和币种;
- 订单摘要;
- 明确的最终确认按钮;
- 成功或失败状态。
这些关键信息不能依赖父页面展示,因为父页面可以随意修改它们。
替换或导航 iframe
父页面可以直接修改 iframe 的地址:
document.querySelector("#payment-frame").src =
"https://phishing.example/fake-payment";
它还可以删除真正的 iframe,再放入一个外观相同的仿冒表单。HTTPS 不会阻止网站修改自己页面中的元素。
第三方支付服务商无法从 iframe 内部强制父页面始终展示真实表单。支付服务能做的是在 iframe 内显示可验证的支付信息,并限制有权嵌入支付页面的站点。
伪造支付结果
如果父页面仅凭一条前端消息判断支付成功,攻击者可能直接伪造这条消息:
window.postMessage(
{
type: "payment.success",
orderId: "ORDER-123"
},
"*"
);
商户后端不能根据浏览器页面、跳转参数或 postMessage 消息直接发货。最终支付状态必须由商户服务器向支付服务器查询,或通过经过签名验证的服务端 webhook 确认。
限制允许嵌入支付页面的网站
支付页面应返回 CSP frame-ancestors 指令,只允许经过审核的商户域名嵌入:
Content-Security-Policy: frame-ancestors https://shop-a.example https://shop-b.example
不建议使用以下无条件开放配置:
Content-Security-Policy: frame-ancestors *
也不要同时使用:
X-Frame-Options: DENY
否则合法商户也无法嵌入支付页面。
X-Frame-Options: ALLOW-FROM 的兼容性和标准化程度不足,不适合作为主要方案。现代浏览器应优先使用 CSP frame-ancestors。
需要注意的是,frame-ancestors 只能解决”哪些 origin 可以嵌入”的问题。它不能保证白名单中的网站永远不会被入侵,也不能阻止获准商户故意设计误导性界面。
如果每个商户使用不同的白名单,可以根据经过验证的商户配置,在服务端动态生成 CSP:
function buildFrameAncestors(merchant) {
const allowedOrigins = merchant.allowedOrigins;
return `frame-ancestors ${allowedOrigins.join(" ")}`;
}
这些 origin 必须来自服务端配置,不能直接读取请求参数,否则攻击者可能把自己的域名加入响应头。
不要信任 iframe URL 中的订单数据
以下设计并不安全:
<iframe
src="https://payments.example/pay?amount=100¤cy=CNY&merchant=shop-a">
</iframe>
即使使用 HTTPS,商户页面也能把 amount=100 改成其他值。支付服务不能把这些查询参数视为可信的订单依据。
更稳妥的做法是让商户后端调用支付服务端,创建支付会话:
POST /api/payment-sessions
Authorization: Bearer merchant_server_credential
Content-Type: application/json
{
"merchantOrderId": "ORDER-123",
"amount": 10000,
"currency": "CNY"
}
支付服务验证商户身份后,返回短期、一次性的会话标识:
{
"sessionId": "ps_7f90b...",
"iframeUrl": "https://payments.example/embed/ps_7f90b..."
}
父页面随后只负责嵌入该地址:
<iframe
id="payment-frame"
src="https://payments.example/embed/ps_7f90b..."
title="安全支付"
allow="payment"
></iframe>
支付会话应在服务端绑定以下信息:
- 商户账户;
- 商户订单号;
- 金额和币种;
- 允许嵌入的父页面 origin;
- 创建时间和过期时间;
- 使用次数;
- 当前支付状态。
客户端提交的金额只能用于展示或交叉检查。最终扣款金额必须取自服务端保存的支付会话。
正确使用 postMessage
跨域 iframe 经常需要通知父页面调整高度或报告支付状态。发送消息时不要使用 "*":
const merchantOrigin = "https://shop-a.example";
window.parent.postMessage(
{
type: "payment.status",
sessionId: "ps_7f90b...",
status: "completed"
},
merchantOrigin
);
父页面接收消息时,必须同时检查 origin 和 source:
const paymentOrigin = "https://payments.example";
const paymentFrame = document.querySelector("#payment-frame");
window.addEventListener("message", (event) => {
if (event.origin !== paymentOrigin) {
return;
}
if (event.source !== paymentFrame.contentWindow) {
return;
}
if (
typeof event.data !== "object" ||
event.data === null ||
event.data.type !== "payment.status"
) {
return;
}
// 只更新界面,不把该消息视为最终支付凭证。
updatePaymentStatus(event.data.status);
});
支付 iframe 接收父页面消息时,也要进行同样的验证。不能因为消息中带有 merchantId 或 sessionId 就信任发送方,这些值可能被复制或猜中。
消息中的支付成功状态只能用来改善用户体验。发货、充值或开通服务必须以服务器端确认为准。
父页面使用 HTTP 时的额外风险
如果顶层页面通过 HTTP 加载,网络攻击者可以修改返回的 HTML 和 JavaScript,包括:
- 修改 iframe 的
src; - 用仿冒支付表单替换真实 iframe;
- 在 iframe 上添加透明覆盖层;
- 修改页面中显示的商品、金额和收款方;
- 注入键盘监听、跳转脚本或恶意广告;
- 篡改支付完成后的展示和跳转;
- 删除所有安全提示或验证代码。
此时,最大的风险并不是 iframe 的 location 被单独修改,而是攻击者控制了整个顶层页面的展示和交互。
HTTPS iframe 嵌入 HTTP 页面,通常不属于”主动混合内容”拦截的典型情况,因为子资源采用了比父页面更安全的协议。但这并不意味着整个流程可信。用户地址栏中显示的仍是 HTTP 父页面,支付服务也无法保证 iframe 周围的内容没有被篡改。
集成政策和服务端配置应明确拒绝 HTTP 商户来源,只登记:
https://merchant.example
不要登记:
http://merchant.example
商户还应将 HTTP 永久重定向到 HTTPS,并在确认所有子域均支持 HTTPS 后启用 HSTS:
Strict-Transport-Security: max-age=31536000; includeSubDomains
HSTS 应由商户站点自行正确部署。支付服务无法通过 iframe 的响应头为父页面开启 HSTS。
Cookie 和浏览器隐私限制
支付 iframe 中的 Cookie 处于第三方上下文。现代浏览器可能限制或彻底阻止第三方 Cookie,因此不能假设普通登录 Cookie 在所有浏览器中都能使用。
如果确实需要跨站 Cookie,通常至少需要设置:
Set-Cookie: payment_session=...; Secure; HttpOnly; SameSite=None
即使设置了 SameSite=None; Secure,Cookie 仍可能受到浏览器隐私策略、用户设置或分区存储机制的影响。
更可靠的设计是为每次嵌入生成短期支付会话令牌,而不是依赖长期第三方 Cookie。令牌应满足以下要求:
- 高熵、不可预测;
- 短期有效;
- 一次性或限制使用次数;
- 与商户、订单和金额绑定;
- 使用后立即失效;
- 不写入可能被记录的敏感数据。
URL 可能出现在浏览器历史、代理日志、分析系统和 Referer 中。因此,即使令牌放在 URL 里,也不能包含银行卡号、身份信息或可长期复用的凭证。支付页面可以设置:
Referrer-Policy: no-referrer
Cache-Control: no-store
iframe 的 sandbox 不能作为核心安全边界
商户可以使用 sandbox 限制 iframe:
<iframe
src="https://payments.example/embed/ps_7f90b..."
sandbox="allow-scripts allow-forms allow-same-origin"
allow="payment"
></iframe>
但 sandbox 属性由父页面控制,恶意商户可以删除或修改它,因此支付服务不能依赖该属性保护自身。
另一方面,过于严格的限制可能导致表单提交、3-D Secure 验证、弹窗或跳转失败。应根据实际支付流程测试所需权限,只申请必要能力。某些支付认证流程可能需要顶层跳转或打开新窗口,这时应明确设计用户手势和目标 origin,避免接受任意跳转地址。
还需要防范支付页面自身的漏洞
跨域隔离无法弥补支付服务自身的安全问题。支付 iframe 仍需按照普通高敏感 Web 应用进行防护,重点包括:
- XSS 和模板注入;
- CSRF;
- SQL 注入及其他服务端注入;
- 会话固定和令牌重放;
- 订单金额、币种及商户身份篡改;
- 重复提交和重复扣款;
- 开放重定向;
- 第三方脚本被入侵;
- 日志、监控或错误信息泄露卡数据;
- webhook 伪造和重放。
支付创建接口应使用幂等键,防止重复扣款:
Idempotency-Key: 80611f8d-4b42-4b46-a9ef-ccd26045f5d1
webhook 应验证签名、时间戳和事件唯一标识,并在数据库层保证同一事件只能处理一次。具体的签名算法和请求格式取决于支付系统协议,不能只根据来源 IP 或请求中的订单号判断真伪。
尽量让银行卡数据只进入支付方控制的页面
银行卡号、有效期和安全码应直接提交给支付服务或合规支付处理机构,不要先进入商户页面,也不要通过 postMessage 把原始卡数据发送给父页面。
推荐的数据流如下:
用户浏览器中的支付 iframe
↓
支付服务或支付处理机构
↓
返回 token / payment method ID
↓
商户后端使用 token 完成后续操作
父页面最多接触不敏感的状态和令牌,不应接触完整卡号或安全码。
这种设计可能缩小商户接触支付数据的范围,但不代表自动满足 PCI DSS。实际合规范围取决于支付表单的字段托管方式、脚本控制权、数据流、支付处理机构方案及适用标准,需要由具备相应资质的人员结合具体架构确认。
建议的最低安全基线
上线前至少应落实以下措施:
- 只接受 HTTPS 商户页面,不支持 HTTP 嵌入。
- 为每个商户维护精确的允许 origin,不使用通配符。
- 使用 CSP
frame-ancestors限制可以嵌入支付页面的站点。 - 由商户后端创建支付会话,不信任 iframe URL 中的金额。
- 使用短期、一次性的会话令牌,并将其与商户、订单、金额和币种绑定。
- 在 iframe 内独立显示收款方、金额、币种和最终确认动作。
- 对
postMessage同时校验origin、source和消息结构。 - 不通过
postMessage传输银行卡号、安全码或长期凭证。 - 支付完成后,由服务器查询支付状态或验证签名 webhook。
- 使用幂等机制防止重复扣款和重复发货。
- 设置
Cache-Control: no-store和严格的Referrer-Policy。 - 限制第三方脚本,并持续防范 XSS、CSRF 和供应链攻击。
- 测试第三方 Cookie 被阻止时的支付流程。
- 根据实际数据流评估 PCI DSS 等合规责任。
iframe 可以有效隔离支付字段,但无法独立构成完整的安全方案。完整的安全边界还需要 HTTPS、浏览器跨域隔离、受控的嵌入来源、服务端订单校验,以及服务器端对支付结果的确认。
备注:内容仅供参考。
