PAYATHON 2026

如何让自建信用卡表单的网站符合 PCI 规范?

支付小周

明确结论

需要考虑 PCI DSS 合规。

只要信用卡号、有效期或安全码经过你控制的网页、JavaScript、服务器、日志系统或数据库,你的系统通常就属于 PCI DSS 的适用范围。即使你最终通过 XML 将数据发送给已通过 PCI DSS 认证的 payment gateway,商户自身的责任也不会因此自动免除。

SSL/TLS 只是 PCI DSS 要求的一项传输安全措施,并不等于”网站自动合规”。防火墙、访问控制、漏洞修复、安全开发、日志审计、恶意脚本防护、定期扫描和合规验证等要求仍可能适用。

更稳妥的方案是使用支付网关提供的 hosted payment page、hosted fields、iframe 或前端 tokenization,让原始卡号直接从用户浏览器发送到网关,不经过你的服务器。

为什么自建信用卡表单会扩大合规范围

判断依据不是表单看起来属于谁,而是持卡人数据实际经过了哪些系统。

如果支付流程是:

用户浏览器
    ↓
你的网站表单
    ↓
你的 Web 服务器
    ↓ XML
Payment Gateway

你的服务器就处理了 PAN(Primary Account Number,主账号,也就是通常所说的卡号)。相关 Web 应用、服务器、网络设备、运维终端、日志平台,以及可能影响这些系统安全的其他系统,都可能被纳入持卡人数据环境,也就是 CDE(Cardholder Data Environment)。

即使程序收到卡号后立即转发,没有写入数据库,这个过程仍属于”处理”或”传输”持卡人数据,不能因此排除 PCI DSS。

更推荐使用下面的流程:

用户浏览器
    ├── 卡号等敏感数据 → Payment Gateway
    │                     ↓
    │                   token
    └── token → 你的服务器 → Payment Gateway

你的服务器只接收 token、订单号、金额和支付结果。这样通常可以明显缩小 PCI DSS 的适用范围。不过,具体应使用哪种 SAQ,仍要根据集成方式以及收单行、支付服务商的认定来确定。

SSL 为什么不够

TLS 可以防止数据在网络传输过程中被轻易窃听,但无法解决下面这些问题:

  • 网站被植入恶意 JavaScript,在加密前读取卡号;
  • 服务器或依赖组件存在可利用的漏洞;
  • 管理员账号使用弱密码,或者没有启用多因素认证;
  • 请求正文、错误日志、APM 或反向代理意外记录完整卡号;
  • 数据库或备份保存了未得到妥善保护的持卡人数据;
  • 员工拥有不必要的数据访问权限;
  • 密钥与加密数据存放在同一位置;
  • 系统缺少补丁管理、漏洞扫描、监控和事件响应机制。

因此,处理持卡人数据时必须使用符合要求的强 TLS,但仅启用 HTTPS 不能证明系统符合 PCI DSS。

各方通常承担什么责任

商户,也就是你

你需要对自己控制的支付页面、应用程序和基础设施负责,通常包括:

  • 梳理持卡人数据流,确认卡号会经过哪些系统、存储在哪些系统;
  • 尽量不接收和存储原始卡号;
  • 根据收单行的要求完成适用的 SAQ,或接受现场评估;
  • 采用安全开发流程,及时修补系统和第三方依赖;
  • 限制对后台、服务器和密钥的访问;
  • 为管理访问启用多因素认证;
  • 防止日志、监控、异常报告和分析工具记录卡号;
  • 按要求进行外部漏洞扫描及其他安全测试;
  • 维护安全策略、事件响应流程,并开展人员培训;
  • 保存支付网关的合规证明及双方的责任划分记录。

如果完整卡号经过你的后端,自评范围通常会大幅增加,并可能涉及 SAQ D。不能仅凭”使用了网关”来判断最终的问卷类型。应由收单行、支付网关的合规团队或 QSA 根据实际数据流确认。

Hosting company

托管服务商一般只对其明确提供和控制的部分负责,例如:

  • 数据中心的物理安全;
  • 底层网络或虚拟化平台;
  • 托管防火墙、负载均衡器等约定服务;
  • 部分操作系统补丁、备份或监控工作。

托管商符合 PCI DSS,不代表部署在其平台上的网站也自动合规。尤其是在普通 VPS、云服务器或自管主机模式下,操作系统、Web 服务器、应用代码、账号、密钥和日志往往仍由商户负责。

你应向托管商索取有效的 AOC(Attestation of Compliance),并确认:

  • 认证是否覆盖你正在使用的具体服务;
  • 哪些 PCI DSS 控制由托管商负责;
  • 哪些控制需要你自行完成;
  • 托管商是否提供书面的 shared responsibility matrix。

Payment Gateway

支付网关通常负责自身支付平台的安全与合规,包括:

  • 安全接收和处理支付数据;
  • 支付授权、清算或 tokenization;
  • 网关侧的访问控制、日志、加密和密钥管理;
  • 提供 AOC 或其他合规证明;
  • 说明推荐的集成方式及对应的合规范围。

你需要确认网关目前具有适用的 PCI DSS 服务提供商资质,不能只看其网站上的”PCI compliant”宣传。网关的合规证明也无法覆盖你的自建表单和服务器。

推荐的解决步骤

1. 优先调整集成架构

可按以下顺序考虑:

  1. 完全托管的支付页面;
  2. 网关托管的 iframe 或 hosted fields;
  3. 由浏览器直接向网关提交卡信息并换取 token;
  4. 只有在其他方案不可行时,才让自己的服务器接收原始卡号。

需要注意,仅在页面上使用网关的 JavaScript SDK,并不能证明卡号没有经过你的系统。必须结合浏览器开发者工具、服务端日志和架构文档,确认请求实际发送到了哪里。

2. 绘制数据流

至少要记录:

  • 卡号从浏览器发送到哪里;
  • 数据是否经过 CDN、WAF、负载均衡器和反向代理;
  • 数据是否进入应用日志、APM、错误跟踪或客服系统;
  • 数据库、缓存、消息队列和备份中是否存在副本;
  • 哪些员工、服务账号和供应商能够访问相关系统。

缺少准确的数据流图,很容易遗漏需要纳入合规范围的系统。

3. 联系收单行确认验证方式

PCI DSS 的具体验证方式通常由收单行或支付服务商根据商户级别、交易量和集成方式确定。不要自行认定某一种 SAQ 就足够。

可能涉及的形式包括:

  • SAQ A;
  • SAQ A-EP;
  • SAQ D for Merchants;
  • ASV 外部漏洞扫描;
  • 渗透测试或其他适用测试;
  • 由 QSA 进行正式评估。

具体适用哪些项目,应以收单行提供的书面要求和当前 PCI DSS 版本为准。

4. 如果必须由后端处理卡号

至少应做到:

  • 全链路使用强 TLS,并正确验证网关证书;
  • 不在 URL、查询参数或 Cookie 中传递卡号;
  • 禁止记录完整的请求正文;
  • 不在数据库、缓存、队列和错误报告中保存安全码;
  • 授权完成后绝不保存 CVV/CVC/CID;
  • 必须保存 PAN 时,使用强加密或不可逆方式保护,并妥善管理密钥;
  • 展示卡号时,只显示业务所需的部分数字;
  • 将支付系统与普通业务系统进行网络隔离;
  • 持续管理代码、依赖和服务器中的漏洞;
  • 监控文件变更、异常访问和支付页面脚本;
  • 建立密钥轮换、账号回收、事件响应和取证流程。

即使加密,授权完成后也不得保存 CVV。所谓”加密后保存 CVV”仍然不合规。

推荐的代码形态

下面的示例使用 tokenized payment。浏览器中的托管字段直接与网关交互,你的后端只会收到 token。示例中的 API 名称仅用于说明,实际使用时必须替换为支付网关的官方 SDK 和接口。

<form id="payment-form">
  <div id="gateway-card-fields"></div>
  <button type="submit">支付</button>
</form>

<script src="https://gateway.example/sdk.js"></script>
<script>
  const fields = Gateway.createHostedFields({
    container: "#gateway-card-fields"
  });

  document
    .querySelector("#payment-form")
    .addEventListener("submit", async (event) => {
      event.preventDefault();

      // 卡号由网关托管字段直接发送给网关。
      const { token } = await fields.tokenize();

      const response = await fetch("/api/payments", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-CSRF-Token": getCsrfToken()
        },
        body: JSON.stringify({
          paymentToken: token,
          orderId: "ORDER-123"
        })
      });

      if (!response.ok) {
        throw new Error("Payment request failed");
      }
    });
</script>

后端只把 token 发送给网关:

app.post("/api/payments", async (req, res) => {
  const { paymentToken, orderId } = req.body;

  // 金额应从服务端订单记录读取,不能信任浏览器提交的金额。
  const order = await loadOrder(orderId);

  const result = await gateway.charge({
    token: paymentToken,
    amount: order.amount,
    currency: order.currency,
    idempotencyKey: order.paymentAttemptId
  });

  res.json({
    status: result.status,
    transactionId: result.transactionId
  });
});

还要确保日志不会输出整个请求对象:

// 不要这样做:请求中一旦混入卡号,就可能被永久写入日志。
logger.info("payment request", req.body);

// 只记录非敏感的必要字段。
logger.info("payment request", {
  orderId,
  transactionId: result.transactionId,
  status: result.status
});

如果支付网关只支持由你的后端通过 XML 传递完整卡号,应使用其官方库和文档规定的 XML 结构,同时关闭 XML 外部实体解析,以防止 XXE。不要将含有真实卡号的 XML 写入日志、调试文件或失败重试队列。

容易忽略的事项

  • 不要使用生产卡号进行开发和测试,应使用网关提供的 sandbox 和测试卡号。
  • 浏览器自动填充、客服截图和前端错误采集也可能泄露卡信息。
  • CDN、WAF 和反向代理能否看到请求正文,会影响它们是否需要纳入评估范围。
  • 第三方统计、聊天、广告和标签管理脚本可能篡改或读取支付页面。
  • tokenization 可以缩小合规范围,但无法消除网站被篡改、订单金额被修改等风险。
  • PCI DSS 合规不是取得一次证书就结束了,还需要持续维护、定期验证并留存证据。

如果业务上没有必须自建卡号处理链路的理由,直接使用网关托管页面或 hosted fields,通常成本更低,风险也更容易控制。

备注:内容仅供参考。