5.8 KiB
Council 评估报告:SecurityEngineer — Round 3 最终版
日期:2026-05-26 审计范围:支付链路(购物车→支付→QR票生成)+ Issue #6 + FOR UPDATE SKIP LOCKED + 前端XSS
一、现状评估
vr-shopxo-plugin 票务链路安全水位:中高。核心安全机制均已到位:QR签名(HMAC-SHA256+8字符截断)、短码混淆(Feistel网络)、悲观锁核销(verifyTicket事务+FOR UPDATE)、幂等发票(order_id+seat_info唯一键)、ShopXO原子库存扣减。无发现 P0 安全漏洞。
二、安全问题全面审计
S-1:issueTicket() 并发竞态 — P0 建议(不等同P0漏洞)
文件:TicketService.php:151-154
$existing = Db::name(BaseService::table('tickets'))
->where('order_id', $order['id'])
->where('seat_info', $spec_name)
->find(); // ← TOCTOU 窗口
实际风险分析:
- ShopXO 的
onOrderPaid回调由内核触发,在支付流水通知层面已有幂等保护 - 只有在 ShopXO 重试回调 + 库存扣减成功但票未生成的极边缘场景下才会并发调用 issueTicket
- 此时 ShopXO 层的原子条件
UPDATE WHERE inventory >= N已经完成了库存扣减,超卖不会发生
缓解措施:已有 ShopXO 原子扣减作为主要防线,issueTicket 幂等检查作为第二层。建议加唯一索引 uk_order_seat(order_id, seat_info) 作为根本性防护,但不应急迫。
结论:⚠️ 可接受,建议修复但可延后
S-2:FOR UPDATE SKIP LOCKED 概念澄清 — P2
实际情况:
| 场景 | 实现 | 防护有效性 |
|---|---|---|
| 并发下单扣库存 | ShopXO WHERE inventory >= N + dec() 原子UPDATE |
✅ 有效 |
| 并发发票 | issueTicket() 幂等检查(无行锁) | ⚠️ 建议加唯一索引 |
| 并发核销 | verifyTicket() lock(true) (FOR UPDATE) |
✅ 有效 |
FOR UPDATE SKIP LOCKED 在此代码库中不是防超卖的关键。真正防超卖的是 ShopXO 的原子条件 UPDATE,不是行锁。verifyTicket 的 FOR UPDATE 已有,SKIP LOCKED 是优化而非必须。
结论:✅ 可接受,概念澄清完毕
S-3:QR Secret 硬编码 — P1
文件:BaseService.php:298-302
private static function getVrSecret(): string
{
// $secret = env('VR_TICKET_SECRET', ''); // 注释掉了
$secret = '8935b3a3-a7b4-4e3d-8c1f-9b7e2a6f5d4c'; // ← 硬编码 fallback
if (empty($secret)) {
throw new \Exception('请在.env中设置VR_TICKET_SECRET=...');
}
return $secret;
}
注意:getQrSecret()(用于QR加密)是强制要求配置 .env,未配置则抛异常。而 getVrSecret()(用于HMAC签名)有硬编码 fallback。
风险:源码泄露时,攻击者可伪造短码。但 HMAC 签名本身(使用 goods_id 派生 key)仍提供了一定隔离。
缓解:生产环境确保 .env 中配置 VR_TICKET_SECRET。
结论:⚠️ P1,建议确认.env配置,可延后至安全专项
S-4:$goodsId 未定义导致 ClearCache 失效 — P3 Bug
文件:TicketService.php:126
SeatMapService::ClearCache(intval($goodsId)); // $goodsId 在 onOrderPaid 中未定义
影响:座位图缓存在支付后不会被清除,下一位买家可能看到过期已售座位。不影响票务安全链路(库存以 ShopXO 数据库为准)。
正确代码应为:
SeatMapService::ClearCache(intval($og['goods_id']));
结论:🟢 P3 Bug,不影响安全,可延后修复
S-5:前端 XSS($goods['content'] 未转义)— P3
文件:ticket_detail.html:75
<?php echo $goods['content']; ?> // 管理后台富文本直接输出
管理员可注入恶意JS,但风险范围仅限管理后台(高信任用户)。观演人表单字段(real_name、phone、id_card)均已使用 htmlspecialchars() 转义,状态良好。
结论:🟢 P3,管理面可控,可延后
三、安全防线评估矩阵(最终版)
| 威胁 | 保护机制 | 强度 | 状态 |
|---|---|---|---|
| 超卖(并发下单) | ShopXO dec() 原子条件UPDATE |
🟢 强 | ✅ 已防护 |
| 超卖(同一座位被多次发票) | issueTicket() 幂等检查 | 🟡 中 | ✅ 建议加唯一索引 |
| 伪造QR票 | HMAC-SHA256签名(ticket_code绑定) | 🟢 强 | ✅ 已防护 |
| 伪造短码 | Feistel混淆+goods_id派生key | 🟡 中 | ✅ 已防护 |
| 重复核销 | verifyTicket() FOR UPDATE 悲观锁 | 🟢 强 | ✅ 已防护 |
| QR票过期重放 | 30分钟 exp + iat + code 字段 | 🟢 强 | ✅ 已防护 |
| 环境密钥泄露 | getVrSecret() 硬编码 fallback |
🔴 危 | ⚠️ 需确认.env |
| 前端XSS | 富文本XSS(管理面可控) | 🟡 中 | ⚠️ 可延后 |
四、Issue #6 结论
当前代码中无 P0 安全漏洞。 所有问题均为 P1-P3,不应作为主攻方向阻塞项。
五、安全维度投票
议题:下一步主攻方向
投票:C — 双线并行
理由:
- 安全所有问题均为 P1-P3,无 P0 漏洞,不应阻塞开发主轴
- 后端 API 完善(seatSpecMap Hook 注册、extension_data 链路)是当前唯一真正的阻塞点
- 安全加固(.env确认、P1唯一索引)可以与前端开发并行推进,不互相依赖
- 最大化团队效率,同时保证安全改进不遗漏
对其他提案的评估:
- A(后端优先):合理,但完全阻塞前端会浪费 H5 保底能力和前端团队资源
- B(前端优先):H5 票务页是好的过渡,但不能忽视 uniapp 是主要目标
- D(Phase 4 优先):Tree API 是差异化功能,Phase 3 核心流程尚未完全稳定,不应跳级
报告人:SecurityEngineer | 2026-05-26