PAYATHON 2026

通过 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&currency=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。

支付 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。实际合规范围取决于支付表单的字段托管方式、脚本控制权、数据流、支付处理机构方案及适用标准,需要由具备相应资质的人员结合具体架构确认。

建议的最低安全基线

上线前至少应落实以下措施:

  1. 只接受 HTTPS 商户页面,不支持 HTTP 嵌入。
  2. 为每个商户维护精确的允许 origin,不使用通配符。
  3. 使用 CSP frame-ancestors 限制可以嵌入支付页面的站点。
  4. 由商户后端创建支付会话,不信任 iframe URL 中的金额。
  5. 使用短期、一次性的会话令牌,并将其与商户、订单、金额和币种绑定。
  6. 在 iframe 内独立显示收款方、金额、币种和最终确认动作。
  7. 对 postMessage 同时校验 origin、source 和消息结构。
  8. 不通过 postMessage 传输银行卡号、安全码或长期凭证。
  9. 支付完成后,由服务器查询支付状态或验证签名 webhook。
  10. 使用幂等机制防止重复扣款和重复发货。
  11. 设置 Cache-Control: no-store 和严格的 Referrer-Policy。
  12. 限制第三方脚本,并持续防范 XSS、CSRF 和供应链攻击。
  13. 测试第三方 Cookie 被阻止时的支付流程。
  14. 根据实际数据流评估 PCI DSS 等合规责任。

iframe 可以有效隔离支付字段,但无法独立构成完整的安全方案。完整的安全边界还需要 HTTPS、浏览器跨域隔离、受控的嵌入来源、服务端订单校验,以及服务器端对支付结果的确认。

备注:内容仅供参考。