前端开发需要关注哪些 PCI DSS 合规要求?
明确结论
前端开发者不必独自负责 PCI DSS 合规,但需要避免让支付卡数据进入无关的前端代码、日志和存储,同时保护支付页面,防止脚本篡改、数据窃取和供应链攻击。
优先考虑支付服务商提供的托管支付页、跳转收银台或 Hosted Fields,让银行卡号、有效期和安全码直接提交给支付服务商。业务系统只接收支付令牌和交易状态。这种方案通常可以大幅缩小 PCI DSS 的评估范围,但商户的合规责任并不会自动消失。具体适用哪种 SAQ、评估覆盖哪些系统,应由收单机构、支付服务商或合格的 PCI 专业人员确认。
如果银行卡数据会经过自己的 DOM、JavaScript、API、服务器或日志系统,这些组件很可能被纳入 PCI DSS 范围,前端团队也要承担更多控制要求。
前端最需要关注的内容
PCI DSS 涵盖人员、流程、网络、应用和基础设施,没有单独针对前端的章节。对前端开发影响较大的内容通常集中在以下方面。
1. 尽量减少支付卡数据的接触范围
前端架构的首要目标不是设法安全保存银行卡号,而是从一开始就不接触它。
常见方案的优先顺序是:
- 支付服务商托管的跳转收银台;
- 支付服务商托管的 iframe 或 Hosted Fields;
- 通过支付服务商 SDK 直接完成令牌化;
- 由自己的前端和后端直接处理银行卡数据。
顺序越靠后,PCI DSS 的覆盖范围和实施成本通常越高。
需要注意的是,如果普通 <input> 由业务页面创建,自己的 JavaScript 读取其中的数据后再传给支付服务商,那么银行卡数据仍然经过了业务前端。即使服务器最终只保存令牌,也通常不能简单地把该页面及相关系统视为没有接触卡数据。
2. 保护支付页面中的脚本
现代 PCI DSS 很重视电商支付页面中的 JavaScript 风险。Magecart 一类攻击可以篡改支付页脚本,在用户提交信息前窃取银行卡数据。
支付页面上的脚本需要满足这些要求:
- 每个脚本都有明确的业务用途和责任人;
- 建立脚本清单,及时删除不再使用的脚本;
- 审批脚本的新增和变更;
- 验证脚本完整性;
- 监测支付页面及安全相关 HTTP 响应头,发现未经授权的变化;
- 严格限制第三方统计、广告、客服和 A/B 测试脚本;
- 管理依赖包、Tag Manager 和 CDN 资源带来的供应链风险。
在 PCI DSS 4.x 中,支付页面脚本管理和页面篡改检测是前端团队尤其需要关注的内容。具体条款、适用日期和替代控制方式可能随标准版本及评估范围变化,应以 PCI SSC 当前发布的正式标准和评估文件为准。
3. 不在浏览器中持久化卡数据
不要把 PAN(Primary Account Number,主账号)、有效期或 CVV/CVC 写入以下位置:
localStoragesessionStorage- IndexedDB
- Cookie
- Service Worker Cache
- URL 查询参数或 URL fragment
- Redux、Pinia、Vuex 等可持久化状态
- 错误追踪事件
- 分析平台自定义属性
- 前端日志和埋点
- 崩溃报告或 Session Replay
- 隐藏表单字段
CVV/CVC 属于敏感认证数据。即使经过加密,通常也不能在授权完成后保存。
如果确实需要展示 PAN,应当进行遮蔽,例如只显示最后四位:
•••• •••• •••• 4242
令牌也不能默认视为非敏感数据。某种令牌能否被排除在 PCI DSS 范围之外,取决于它是否可逆、如何使用,以及支付服务商采用了怎样的令牌化设计。
推荐的支付架构
下面是一种常见且风险较低的支付流程:
用户浏览器
├─ 业务页面:订单、金额、商品信息
├─ Hosted Fields:银行卡数据直接进入支付服务商
└─ 获得一次性 payment token
↓
业务后端:接收 token,创建支付请求
↓
支付服务商:处理银行卡授权
↓
Webhook:通知业务后端最终交易状态
实施时要注意:
- 业务前端不读取银行卡字段;
- 业务后端不接收明文银行卡号或 CVV;
- 前端提交一次性令牌,不提交支付卡数据;
- 由后端查询或经过签名验证的 Webhook 确认支付结果;
- 不信任浏览器传来的金额、币种、折扣和支付状态。
以下代码只是概念性示例。真实的 API 名称和参数必须以所选支付服务商的官方 SDK 为准,不能直接照搬:
<form id="payment-form">
<div id="card-number-hosted-field"></div>
<div id="expiry-hosted-field"></div>
<div id="security-code-hosted-field"></div>
<button type="submit">支付</button>
</form>
const paymentForm = document.querySelector('#payment-form');
paymentForm.addEventListener('submit', async (event) => {
event.preventDefault();
// paymentProvider 代表支付服务商提供的官方 SDK。
// 银行卡数据应由 Hosted Fields 直接提交给服务商。
const { token, error } = await paymentProvider.createPaymentToken();
if (error) {
showPaymentError('无法验证支付信息,请检查后重试');
return;
}
const response = await fetch('/api/payments', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({
orderId: currentOrderId,
paymentToken: token
})
});
if (!response.ok) {
showPaymentError('支付请求未完成,请稍后重试');
return;
}
showPaymentPending();
});
后端必须根据 orderId 重新读取商品、金额和币种,不能直接采用前端提交的价格:
app.post('/api/payments', requireAuthenticatedUser, async (req, res) => {
const order = await orderRepository.findForUser(
req.body.orderId,
req.user.id
);
if (!order || order.status !== 'pending') {
return res.status(400).json({ error: 'Invalid order' });
}
const payment = await paymentProvider.createPayment({
token: req.body.paymentToken,
amount: order.totalAmount,
currency: order.currency,
idempotencyKey: order.id
});
res.status(202).json({
paymentId: payment.id,
status: 'pending'
});
});
支付令牌应有较短的有效期和明确的用途限制。支付请求还应使用幂等键,避免用户重复点击或网络重试导致重复扣款。
浏览器安全控制
Content Security Policy
严格的 CSP 可以减少脚本注入和未知第三方脚本的执行,但不能单独证明系统符合 PCI DSS,也无法阻止已获准加载的第三方脚本遭到恶意修改。
可以先采用较严格的策略,再按照支付服务商实际使用的域名逐项放行:
Content-Security-Policy:
default-src 'none';
script-src 'self' 'nonce-{RANDOM_NONCE}' https://js.payment-provider.example;
frame-src https://checkout.payment-provider.example;
connect-src 'self' https://api.payment-provider.example;
img-src 'self' data:;
style-src 'self' 'nonce-{RANDOM_NONCE}';
base-uri 'none';
object-src 'none';
form-action 'self' https://checkout.payment-provider.example;
frame-ancestors 'self';
服务器必须为每个响应生成不可预测的 {RANDOM_NONCE}。不要为了兼容旧代码轻易加入 'unsafe-inline' 或 'unsafe-eval'。
可以先通过 Content-Security-Policy-Report-Only 收集和观察违规报告,再逐步启用阻断策略。报告中可能包含页面地址等信息,因此采集和保存报告时也要防止敏感数据泄露。
Subresource Integrity
对于版本固定且支持跨域校验的静态资源,可以使用 SRI:
<script
src="https://cdn.example.com/payment-helper-1.2.3.min.js"
integrity="sha384-REPLACE_WITH_REAL_HASH"
crossorigin="anonymous">
</script>
哈希值必须取自经过验证的发布产物。支付 SDK 的内容如果会动态变化,SRI 可能并不适用。此时应按照服务商的建议加载,并结合脚本清单、CSP、变更检测和供应链审查来降低风险。
iframe 通信
使用支付 iframe 时,必须严格验证 postMessage 的来源:
const PAYMENT_ORIGIN = 'https://checkout.payment-provider.example';
window.addEventListener('message', (event) => {
if (event.origin !== PAYMENT_ORIGIN) {
return;
}
if (!isValidPaymentMessage(event.data)) {
return;
}
handlePaymentMessage(event.data);
});
发送消息时不要使用通配符作为目标来源:
paymentFrame.contentWindow.postMessage(
{ type: 'START_PAYMENT', orderId: currentOrderId },
PAYMENT_ORIGIN
);
除了验证来源,还要验证消息结构。即使来源正确,也不能直接信任 event.data。
认证信息也要单独保护
“其他凭证信息”不一定属于支付卡数据,但仍需按照应用安全要求进行保护:
- 密码只能提交给后端,不能在前端加密后自行保存;
- 登录和支付接口全程使用 HTTPS;
- 会话 Cookie 设置
Secure、HttpOnly和合适的SameSite; - 防御 CSRF、XSS、点击劫持和开放重定向;
- 不在 URL 中放置访问令牌;
- 避免把长期令牌存入
localStorage; - 支付、退款和修改收货信息等敏感操作需要重新验证身份;
- 为登录、支付和验证码接口设置速率限制;
- 错误提示不能暴露账户是否存在、风控规则或支付网关的内部响应。
例如:
Set-Cookie: session=opaque-value; Path=/; Secure; HttpOnly; SameSite=Lax
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
这些参数要结合登录流程、跨站支付跳转和子域管理进行调整。例如,某些跨站回调可能需要采用不同的 SameSite 策略。
日志、监控和测试中的常见泄露点
支付卡数据不一定泄露自数据库,也可能进入各种辅助系统。需要检查:
console.log()和网络调试日志;- Sentry 等错误追踪工具;
- Session Replay 和用户行为录屏;
- CDN、WAF、反向代理和 API Gateway 日志;
- 浏览器开发工具导出的 HAR 文件;
- 客服工单、截图和聊天记录;
- 自动化测试录像及失败快照;
- 测试夹具、Storybook 示例和 Git 仓库;
- CI/CD 构建日志;
- URL、页面标题和埋点字段。
不要记录完整的支付请求:
// 错误示例
logger.info('payment submitted', formData);
只记录排查问题所需的非敏感信息:
logger.info('payment submitted', {
orderId,
provider: 'example-provider',
result: 'token_created'
});
测试环境应使用支付服务商提供的测试卡号和沙箱。不要把生产卡数据复制到开发、测试或演示环境。
前端团队可执行的落地清单
可以按以下顺序实施:
- 绘制银行卡数据流,标明数据如何在浏览器、第三方脚本、后端、日志系统和支付服务商之间流动。
- 确认业务系统是否接触明文 PAN、有效期或 CVV。
- 优先选择经过验证的托管收银台或 Hosted Fields。
- 与合规负责人、收单机构或支付服务商确认 PCI DSS 范围及适用的 SAQ。
- 为支付页面建立脚本和第三方依赖清单。
- 删除支付页面中非必要的广告、统计、客服和标签管理脚本。
- 部署 CSP,并监测脚本及安全相关响应头的变化。
- 禁止支付卡数据进入浏览器存储、URL、日志、监控系统和 Session Replay。
- 检查 XSS、CSRF、依赖漏洞、DOM 注入和
postMessage校验。 - 对支付接口实施身份验证、服务端金额校验、幂等控制和速率限制。
- 使用经过签名验证的 Webhook 或服务端查询确认最终支付结果。
- 保留代码审查、脚本审批、扫描、测试和变更记录,供合规评估使用。
容易产生的误解
- 使用 HTTPS 就合规了:HTTPS 只解决传输保护中的一部分问题。
- 不保存卡号就不在 PCI DSS 范围内:只要系统传输或处理过卡数据,仍有可能被纳入范围。
- 使用 iframe 就自动合规:父页面遭到篡改后,攻击者仍可能替换 iframe、修改提交目标或展示伪造表单。
- 接入合规的支付服务商后商户不再负责:外包支付处理可以缩小范围,但商户仍需管理集成方式、第三方服务和自身页面的安全。
- 通过一次漏洞扫描就算完成:PCI DSS 要求持续实施控制,包括资产管理、变更管理、访问控制、监测和定期验证。
- 前端把数据加密后发送就足够了:只要业务 JavaScript 能读取明文数据,攻击者控制页面脚本后就可能在加密前将其窃取。
参考资料
应优先查阅 PCI Security Standards Council 官方发布的以下资料:
- 当前版本的《PCI DSS Requirements and Testing Procedures》;
- PCI DSS SAQ 文档及适用性说明;
- 电商支付页面安全、脚本攻击和数据窃取防护指南;
- Tokenization、P2PE 和第三方服务商相关指南;
- PCI SSC FAQ;
- 所选支付服务商提供的 PCI DSS、Hosted Fields、Webhook 验签和安全集成文档;
- OWASP ASVS、OWASP Top 10 和 OWASP Third Party JavaScript Management Cheat Sheet。
PCI DSS 的范围取决于实际数据流、支付集成方式、商户等级、收单机构要求和第三方服务关系。前端团队可以落实许多重要控制,但不能只根据代码自行作出最终的合规判断。
备注:内容仅供参考。
