如何安全收集信用卡信息但不处理付款?
明确结论
不要在自己的 PHP、MySQL、CSV 或 email 流程中收集、保存或传递完整的信用卡号、有效期和安全码。
即使公司不扣款,只是代酒店收集并转交信息,也不能免除安全责任。只要系统接触信用卡信息,就可能被纳入 PCI DSS 合规范围。发生泄露后,公司还可能承担通知、调查、赔偿、罚款和声誉损失。
更安全、可行的方案是:
- 由酒店直接向参会人员收集信用卡信息;
- 使用酒店认可且符合 PCI DSS 要求的服务,通过托管页面收集信息;
- 公司系统只保存报名资料和不具备支付能力的引用信息,如酒店确认号、第三方 token、处理状态和卡号后四位。
如果酒店无法提供安全的收集渠道,应由公司管理层、法务、安全负责人,以及收单机构或合格安全评估机构(QSA)共同确定处理方案。不要让普通报名系统临时承担银行卡信息保管功能。
为什么“只转发给酒店”仍然不安全
可以这样向非技术人员解释:
信用卡号就像酒店房间的万能钥匙。即使公司不准备使用这把钥匙,只要代为保管、复制或发送,就必须为钥匙丢失承担责任。把信用卡号存进数据库、CSV 或邮件,相当于复制了多把钥匙,并把它们留在服务器、员工电脑、邮箱和备份中。
一次报名可能留下许多副本:
- 浏览器提交的内容;
- Web server 或反向代理日志;
- PHP 错误日志;
- MySQL 数据库及其备份;
- CSV 导出文件;
- admin 的收件箱和邮件服务器;
- 邮件转发、归档和垃圾邮件过滤系统;
- 员工电脑的下载目录;
- 调试工具、监控平台和工单截图。
即使数据库已经加密,应用程序仍需通过某种方式解密数据。如果攻击者同时获得应用权限或密钥,依然可以读取卡号。加密只能减少部分风险,不能自动让系统变得安全或符合 PCI DSS 要求。
CSV 和 email 为什么尤其危险
不要把完整卡号写入 CSV,也不要通过普通 email 发送完整卡号或安全码。
CSV 文件通常缺少以下保护:
- 访问权限控制;
- 操作审计;
- 自动过期;
- 防复制措施;
- 集中删除能力;
- 密钥管理。
email 的风险更大。邮件会经过多个系统,并可能长期保留在服务器、备份和用户终端上。传输过程使用 TLS,并不代表只有指定收件人能够读取,也无法防止转发、误发或邮箱被盗。
PCI DSS 并未简单地把 email 视为传输银行卡信息的安全渠道。通过消息技术传输未受保护的 PAN(Primary Account Number)时,需要采取相应的保护措施。安全码等敏感认证数据还受到更严格的限制。不要把“压缩包加密码”或“邮件启用 TLS”当作完整的解决方案。
推荐的业务流程
方案一:由酒店直接收集
应优先选择这种方案。报名系统完成普通资料登记后:
- 创建不含银行卡信息的报名记录;
- 将用户跳转到酒店提供的安全页面;
- 由酒店直接收集信用卡信息;
- 酒店返回预订确认号或处理状态;
- 系统保存确认号,不保存卡号。
如果酒店没有在线页面,可以要求酒店向参会人员发送安全的付款授权链接,或者提供受控的电话收集流程。
方案二:使用酒店认可的第三方托管页面
第三方页面应运行在公司服务器之外。银行卡信息必须由用户的浏览器直接提交给服务商,不能先经过公司的 PHP endpoint。
接入前需要确认:
- 服务商是否持有适用于该服务范围的 PCI DSS 合规证明,如 AOC;
- 服务是否明确支持“酒店预订担保”或延迟授权场景;
- 生成的 token 能否供目标酒店使用;
- 酒店通过什么方式获得数据;
- 公司系统是否会接触完整 PAN;
- 数据保存期限、删除方式和事件响应责任;
- iframe、redirect、Hosted Fields 等集成方式会如何影响 PCI DSS 范围。
普通支付网关生成的 token 通常只能在该网关和指定商户账户中使用。不能想当然地认为酒店收到 token 后,就能通过自己的系统取得卡号或完成担保。
方案三:使用第三方安全保管库,并由酒店受控访问
只有在酒店确实无法直接收集信息时,才考虑使用专门的 card vault 或支付授权服务:
- 由第三方托管表单收集银行卡信息;
- 公司只接收不可逆的引用 ID;
- 酒店通过受控 portal 或正式 API 获取所需授权;
- 每次访问都需要身份验证,并保留审计记录;
- 信息按照约定期限自动删除;
- CSV 和 email 中仍不出现完整卡号。
这种方案必须得到酒店和第三方服务商的明确支持。不要自行设计“加密数据库 + 管理员下载解密”作为替代方案。
PHP 集成示例
以下内容只是架构示例,不对应任何特定服务商。实际使用的 endpoint、签名算法和字段必须以所选服务商的官方文档为准。
报名表只接收普通信息:
<form method="post" action="/register.php">
<label>
Name
<input type="text" name="name" required>
</label>
<label>
Email
<input type="email" name="email" required>
</label>
<button type="submit">Continue to hotel guarantee</button>
</form>
PHP 创建报名记录后,将用户跳转到酒店或第三方托管页面:
<?php
declare(strict_types=1);
session_start();
$name = trim($_POST['name'] ?? '');
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($name === '' || $email === false || $email === null) {
http_response_code(400);
exit('Invalid registration data');
}
// 使用 prepared statement 保存普通报名资料。
// 数据表中不要设置 card_number、cvv、track_data 等字段。
$registrationId = createRegistration($name, $email);
$_SESSION['registration_id'] = $registrationId;
// 此 URL 应由酒店或合规服务商生成。
// production 环境不得使用自行拼接的敏感参数。
$hostedUrl = createHostedReservationUrl([
'registration_reference' => (string) $registrationId,
'return_url' => 'https://conference.example/booking-complete',
]);
header('Location: ' . $hostedUrl, true, 303);
exit;
系统接收第三方回调时,只保存状态、确认号、token 或 masked PAN:
<?php
declare(strict_types=1);
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_PROVIDER_SIGNATURE'] ?? '';
if (!verifyWebhookSignature($rawBody, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);
if (($event['type'] ?? '') === 'reservation.guaranteed') {
updateRegistration([
'registration_reference' => $event['registration_reference'],
'reservation_status' => 'guaranteed',
'hotel_confirmation' => $event['hotel_confirmation'],
'payment_reference' => $event['payment_reference'],
'card_brand' => $event['card_brand'] ?? null,
'card_last4' => $event['card_last4'] ?? null,
]);
}
http_response_code(204);
导出的 CSV 只能包含非敏感资料:
registration_id,name,email,reservation_status,hotel_confirmation,card_brand,card_last4
1042,Example User,user@example.com,guaranteed,HOTEL-88321,Visa,1234
通知邮件也只能发送摘要和后台链接:
Subject: New conference registration
Registration: 1042
Name: Example User
Reservation status: Guaranteed
Hotel confirmation: HOTEL-88321
Card: Visa ending in 1234
Review:
https://conference.example/admin/registrations/1042
访问后台链接时,应要求用户登录并通过多因素认证,同时实施最小权限控制和审计日志。不要在邮件中放入第三方的长期访问凭证,也不要发送能够兑换出完整卡号的 token。
必须避免的实现方式
以下做法都不能作为解决方案:
- 将卡号明文写入 MySQL;
- 将卡号“临时”写入数据库后再删除;
- 把完整卡号放入 CSV;
- 通过普通 email、聊天软件或工单传递卡号;
- 收集 CVV 后长期保存;
- 自行编写 AES 加密功能,并据此认为系统已经符合 PCI DSS 要求;
- 把加密密钥与应用代码、配置文件或数据库放在同一个控制边界内;
- 在 access log、exception log、analytics 或 error-reporting 服务中记录表单内容;
- 使用前端 JavaScript 隐藏字段,却仍把卡号提交到自己的服务器;
- 只依靠 HTTPS。HTTPS 只能保护传输链路,无法解决存储、邮件、导出、权限和终端泄露等问题。
合规和管理层需要确认的事项
PCI DSS 的具体适用范围取决于数据流、服务商的集成方式、酒店与公司的合同关系,以及谁会被认定为 merchant 或 service provider。应向收单机构、酒店的支付负责人或 QSA 获取针对该流程的正式判断。
至少需要书面确认:
- 谁负责收集和保管银行卡信息;
- 公司是否会接触完整 PAN;
- 是否收集 CVV,以及何时删除;
- 酒店是否接受第三方 token;
- 哪一方负责身份验证、日志和访问审计;
- 数据保留期限;
- 发生泄露时由谁响应和通知;
- 第三方的 PCI DSS 合规状态;
- 是否还受到 GDPR、州隐私法或当地个人信息保护法规的约束。
还需要特别注意,CVV/CVC 属于敏感认证数据,授权完成后不能继续保存,即使经过加密也不行。具体业务能否在授权前短暂处理 CVV,以及酒店担保何时构成授权,应由支付服务商和 QSA 判断,不能由开发人员自行推定。
给管理层的简短说明
可以直接使用下面这段话:
公司是否实际扣款并不是关键。只要我们的网页、服务器、数据库、邮件或导出文件接触完整信用卡信息,公司就必须保护整个数据流,并可能承担 PCI DSS 合规和数据泄露责任。每导出一次 CSV 或发送一次邮件,都会增加一份难以控制和删除的信用卡副本。更安全的做法是让酒店或经过验证的 PCI DSS 合规服务商直接收集信息,我们只保存预订状态、确认号和卡号后四位。
权威资料
可将以下资料提供给管理层、法务和安全负责人:
- PCI Security Standards Council:PCI DSS 官方资料库
- PCI Security Standards Council:面向小型商户的安全资源
- PCI Security Standards Council:常见问题
- PCI SSC FAQ:通过电子邮件、短信等终端用户消息技术发送持卡人数据
- PCI SSC FAQ:敏感认证数据的保存限制
最终设计应确保公司的报名系统始终不会收到完整的信用卡信息。做到这一点后,CSV、email 和普通报名管理功能仍可继续使用,但其中只能包含非敏感资料、处理状态和受限制的引用信息。
备注:内容仅供参考。
