Compare commits
5 Commits
main
...
council/Se
| Author | SHA1 | Date |
|---|---|---|
|
|
52604625d8 | |
|
|
7b6942f8d0 | |
|
|
8b4efd705c | |
|
|
cec3b09531 | |
|
|
8eeeb72f03 |
|
|
@ -0,0 +1,206 @@
|
||||||
|
# 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`
|
||||||
|
|
||||||
|
```php
|
||||||
|
$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`
|
||||||
|
|
||||||
|
```php
|
||||||
|
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`
|
||||||
|
|
||||||
|
```php
|
||||||
|
SeatMapService::ClearCache(intval($goodsId)); // $goodsId 在 onOrderPaid 中未定义
|
||||||
|
```
|
||||||
|
|
||||||
|
**影响**:座位图缓存在支付后不会被清除,下一位买家可能看到过期已售座位。**不影响票务安全链路**(库存以 ShopXO 数据库为准)。
|
||||||
|
|
||||||
|
**正确代码应为**:
|
||||||
|
```php
|
||||||
|
SeatMapService::ClearCache(intval($og['goods_id']));
|
||||||
|
```
|
||||||
|
|
||||||
|
**结论**:🟢 **P3 Bug,不影响安全,可延后修复**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### S-5:前端 XSS($goods['content'] 未转义)— P3
|
||||||
|
|
||||||
|
**文件**:`ticket_detail.html:75`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?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 — 双线并行**
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. 安全所有问题均为 P1-P3,无 P0 漏洞,**不应阻塞开发主轴**
|
||||||
|
2. 后端 API 完善(seatSpecMap Hook 注册、extension_data 链路)是当前唯一真正的阻塞点
|
||||||
|
3. 安全加固(.env确认、P1唯一索引)可以与前端开发并行推进,不互相依赖
|
||||||
|
4. 最大化团队效率,同时保证安全改进不遗漏
|
||||||
|
|
||||||
|
**对其他提案的评估**:
|
||||||
|
- **A(后端优先)**:合理,但完全阻塞前端会浪费 H5 保底能力和前端团队资源
|
||||||
|
- **B(前端优先)**:H5 票务页是好的过渡,但不能忽视 uniapp 是主要目标
|
||||||
|
- **D(Phase 4 优先)**:Tree API 是差异化功能,Phase 3 核心流程尚未完全稳定,不应跳级
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、Round 4 现场核查确认
|
||||||
|
|
||||||
|
### 安全行动项现场核实
|
||||||
|
|
||||||
|
**S-4 Bug 确认**(`TicketService.php:126`):
|
||||||
|
```php
|
||||||
|
SeatMapService::ClearCache(intval($goodsId)); // $goodsId 未定义
|
||||||
|
```
|
||||||
|
✅ 确认存在。正确代码应为 `$og['goods_id']`。此 Bug 不影响安全(ShopXO DB 库存为准),但会导致座位图缓存失效迟缓,影响用户体验。
|
||||||
|
|
||||||
|
**S-3 QR Secret 硬编码确认**(`BaseService.php:302`):
|
||||||
|
```php
|
||||||
|
$secret = '8935b3a3-a7b4-4e3d-8c1f-9b7e2a6f5d4c'; // 确认存在
|
||||||
|
```
|
||||||
|
✅ 确认存在。生产环境需配置 `.env` 的 `VR_TICKET_SECRET`。
|
||||||
|
|
||||||
|
**S-1 幂等检查确认**(`TicketService.php:151-154`):
|
||||||
|
```php
|
||||||
|
$existing = Db::name(BaseService::table('tickets'))
|
||||||
|
->where('order_id', $order['id'])
|
||||||
|
->where('seat_info', $spec_name)
|
||||||
|
->find(); // 无行锁,但有幂等保护
|
||||||
|
```
|
||||||
|
✅ 确认存在。ShopXO `onOrderPaid` 由内核回调触发,已在支付流水层有幂等保护。唯一建议是加唯一索引 `uk_order_seat(order_id, seat_info)`,但可延后。
|
||||||
|
|
||||||
|
### BackendArchitect P0 重分类背书
|
||||||
|
|
||||||
|
BackendArchitect Round 4 修正了 Round 1-3 的 P0 评估:
|
||||||
|
- `getSoldSeats()` 方法缺失 → **已消除**(`SeatMapService::GetSeatMap()` 已含库存)
|
||||||
|
- `Index.php::soldSeats` Fatal Error → **已消除**(Index.php 无此 action)
|
||||||
|
- `plugins_service_goods_data` Hook 未注册 → **降为 P1**(UniApp 可用 `/seatmap` 变通)
|
||||||
|
|
||||||
|
我**背书 BackendArchitect 的 P0 重分类**。与我的安全评估一致:无 P0 安全漏洞。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、安全维度投票(Round 4 确认)
|
||||||
|
|
||||||
|
**议题:下一步主攻方向**
|
||||||
|
**投票:C(双线并行)**(Round 1/2/3/4 一致,不变)
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. 安全所有问题均为 P1-P3,**无 P0 安全漏洞,不应阻塞开发主轴**
|
||||||
|
2. 后端 P0 重分类(无 P0)确认安全不阻塞;后端 API 完善(Hook 注册、seatSpecMap 注入)是当前真正阻塞点
|
||||||
|
3. 安全加固(.env 确认、P1 唯一索引)可与前端开发并行推进,不互相依赖
|
||||||
|
4. H5 `ticket_detail.html` 可作为独立过渡方案,立即推进 `loadSoldSeats()` 实现
|
||||||
|
|
||||||
|
**补充**:BackendArchitect 投票 A(后端优先)有合理性(Hook 注册是最小改动最大收益),但前端 H5 无阻塞可立即推进,两者不冲突。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*报告人:SecurityEngineer | 2026-05-26 | Round 4 核查完成*
|
||||||
120
plan.md
120
plan.md
|
|
@ -1,109 +1,41 @@
|
||||||
# Plan — 调研「场馆删除后编辑商品出现规格重复错误」问题
|
# Plan — vr-shopxo-plugin 安全评估 + 票务链路审计
|
||||||
|
|
||||||
> 版本:v1.3 | 日期:2026-04-20 | Agent:council/FrontendDev + council/SecurityEngineer + council/BackendArchitect
|
> 版本:v3.0 | 日期:2026-05-26 | Agent:council/SecurityEngineer
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## BackendArchitect(Task B1-B6)
|
## 安全评估结论(Round 3 最终版)
|
||||||
|
|
||||||
当票务商品关联的场馆模板被硬删除后,编辑商品时出现「规格不允许重复」错误。
|
**核心结论:支付链路安全水位中高,无 P0 漏洞。安全维度不应作为主攻方向阻塞项。**
|
||||||
|
|
||||||
**根因调查分工**:
|
| # | 问题 | 严重性 | 结论 |
|
||||||
- FrontendDev:前端规格项构建与 fallback 行为
|
|---|------|--------|------|
|
||||||
- BackendArchitect:后端规格去重逻辑、`spec_base_id_map` 解析
|
| S-1 | issueTicket() 并发竞态 | P0 建议 | 建议加唯一索引,可延后 |
|
||||||
- SecurityEngineer:安全风险评估(P1 vs P2)
|
| S-2 | FOR UPDATE SKIP LOCKED 概念澄清 | P2 | 概念混淆,防超卖依赖ShopXO原子UPDATE已有效 |
|
||||||
|
| S-3 | getVrSecret() 硬编码 fallback | P1 | 需确认生产环境 .env 配置 |
|
||||||
|
| S-4 | $goodsId 未定义导致 ClearCache 失效 | P3 Bug | 不影响票务安全,可延后 |
|
||||||
|
| S-5 | $goods['content'] XSS(管理面可控) | P3 | 管理面可控,可延后 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## FrontendDev 任务清单
|
## 安全行动项(优先级排序)
|
||||||
|
|
||||||
- [x] [Done: council/FrontendDev] **Task 1**: 读取 `ticket_detail.html`,分析前端构建规格项的过程
|
| 优先级 | 行动 | 说明 |
|
||||||
- [x] [Done: council/FrontendDev] **Task 2**: 当模板不存在时,前端如何处理 `template_snapshot` 和 `spec_base_id_map`?
|
|--------|------|------|
|
||||||
- [x] [Done: council/FrontendDev] **Task 3**: `loadSoldSeats()` 函数实际实现了吗?soldSeats 数据如何填充?
|
| P1 | 确认生产环境 `.env` 配置 `VR_TICKET_SECRET` | 防止源码泄露后伪造票 |
|
||||||
- [x] [Done: council/FrontendDev] **Task 4**: 编辑模式下(已有 vr_goods_config),前端是否正确处理已删除场馆的旧规格?
|
| P1 | 添加唯一索引 `uk_order_seat(order_id, seat_info)` | 从根本上防止并发发票竞态 |
|
||||||
- [x] [Done: council/FrontendDev] **Task 5**: 给出前端根因分析(含具体文件路径和行号)
|
| P3 | 修复 `ClearCache($goodsId)` Bug | 使用 `$og['goods_id']` |
|
||||||
- [x] [Done: council/FrontendDev] **Task 6**: 给出修复方案
|
| P3 | `$goods['content']` 转义 | 防止富文本XSS |
|
||||||
- [x] [Done: council/FrontendDev] **Task 7**: 将调研报告写入 `reviews/council-ghost-spec-FrontendDev.md`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SecurityEngineer 任务清单
|
## 投票
|
||||||
|
|
||||||
- [x] [Done: council/SecurityEngineer] **Task S1**: 读取 AdminGoodsSaveHandle.php — 安全审计:保存时是否拒绝脏数据
|
**议题:下一步主攻方向**
|
||||||
- [x] [Done: council/SecurityEngineer] **Task S2**: 读取 SeatSkuService.php — 幽灵 spec 注入路径分析
|
**投票:C(双线并行)**(Round 1/2/3/4 一致,不变)
|
||||||
- [x] [Done: council/SecurityEngineer] **Task S3**: 读取 AdminGoodsSave.php — ShopXO 入口安全检查
|
|
||||||
- [x] [Done: council/SecurityEngineer] **Task S4**: 输出安全审计报告 → `reviews/SecurityEngineer-GHOST_SPEC_SECURITY.md`
|
|
||||||
- [x] [Done: council/SecurityEngineer] **Task S5**: 更新 `reviews/council-ghost-spec-summary.md`
|
|
||||||
|
|
||||||
### 优先级定义
|
**Round 4 核查确认**:
|
||||||
|
- S-4 Bug 确认,`ClearCache($goodsId)` 应为 `ClearCache($og['goods_id'])`
|
||||||
| 级别 | 含义 |
|
- S-3 硬编码确认,生产需配置 `.env`
|
||||||
|------|------|
|
- S-1 幂等已有 ShopXO 保护,延后加唯一索引
|
||||||
| **P1** | 安全漏洞:脏数据注入、XSS、权限绕过、数据覆盖 |
|
- BackendArchitect P0 重分类背书:无 P0 安全漏洞
|
||||||
| **P2** | 功能缺陷:用户体验问题、错误提示不友好 |
|
|
||||||
| **P3** | 改进建议:代码健壮性优化 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BackendArchitect 任务清单
|
|
||||||
|
|
||||||
- [x] [Done: council/BackendArchitect] **Task B1**: AdminGoodsSaveHandle.php 全链路追踪 — vr_goods_config 读取/解析/snapshot 重建
|
|
||||||
- [x] [Done: council/BackendArchitect] **Task B2**: spec_base_id_map 如何被转换成规格项(已验证:存储在模板表,与幽灵 spec 无关)
|
|
||||||
- [x] [Done: council/BackendArchitect] **Task B3**: SeatSkuService GetGoodsViewData 模板不存在时的 fallback(单模板处理,多模板有缺陷)
|
|
||||||
- [x] [Done: council/BackendArchitect] **Task B4**: 幽灵 spec 产生环节 + 清理时机(保存时未清理,写回 DB)
|
|
||||||
- [x] [Done: council/BackendArchitect] **Task B5**: 商品保存规格去重逻辑(GoodsService.php:1859)
|
|
||||||
- [x] [Done: council/BackendArchitect] **Task B6**: 根因分析报告(含行号)→ `reviews/council-ghost-spec-BackendArchitect.md`
|
|
||||||
- [x] [Done: council/BackendArchitect] **Task B7**: 将调研报告写入 `reviews/council-ghost-spec-BackendArchitect.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 阶段划分 ✅
|
|
||||||
|
|
||||||
| 阶段 | 内容 | 状态 |
|
|
||||||
|------|------|------|
|
|
||||||
| **Draft** | Task 1-7(FrontendDev)+ Task S1-S3 + Task B1-B6(并行)| ✅ 完成 |
|
|
||||||
| **Review** | Task 7 + Task S4 + Task B7(输出各自报告)| ✅ 完成 |
|
|
||||||
| **Finalize** | Task S5:汇总到 `reviews/council-ghost-spec-summary.md` | ✅ 完成 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 根因结论
|
|
||||||
|
|
||||||
| 优先级 | 根因 | 文件:行号 |
|
|
||||||
|--------|------|-----------|
|
|
||||||
| **P1(功能)** | 无效 config 块未从数组移除,`continue` 后脏数据写回 DB | AdminGoodsSaveHandle.php:88-89 + 148-150 |
|
|
||||||
| **P2** | GetGoodsViewData 单模板模式,多模板时覆盖有效块 | SeatSkuService.php:368 + 386-388 |
|
|
||||||
| **P3** | BatchGenerate 对无效 template_id 返回 code=-2,阻断保存 | AdminGoodsSaveHandle.php:164-170 |
|
|
||||||
| **P4** | 前端过滤后 configs 为空时用户无声失去配置 | AdminGoodsSave.php:196-229 |
|
|
||||||
| **P5** | loadSoldSeats 未实现(TODO 注释) | ticket_detail.html:375-383 |
|
|
||||||
| **安全评估** | 无 P1 安全漏洞,属于 P2 功能缺陷 | SecurityEngineer-GHOST_SPEC_SECURITY.md |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 关键文件
|
|
||||||
|
|
||||||
| 文件 | 关注点 |
|
|
||||||
|------|--------|
|
|
||||||
| `shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php` | P1 根因:continue 不删除脏 config |
|
|
||||||
| `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php` | GetGoodsViewData:P2 根因,多模板处理缺陷 |
|
|
||||||
| `shopxo/app/plugins/vr_ticket/hook/AdminGoodsSave.php` | 前端过滤逻辑:P4 体验问题 |
|
|
||||||
| `shopxo/app/plugins/vr_ticket/admin/Admin.php` | VenueDelete:硬删除逻辑(第 888 行) |
|
|
||||||
| `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html` | loadSoldSeats 未实现(P5) |
|
|
||||||
| `shopxo/app/service/GoodsService.php` | 规格列值去重检测(第 1859 行) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 修复方案
|
|
||||||
|
|
||||||
### P1 Fix(立即实施)
|
|
||||||
1. AdminGoodsSaveHandle.php:88 — `continue` 改为 `unset($configs[$i])`
|
|
||||||
2. AdminGoodsSaveHandle.php:145 后 — 添加 `$configs = array_values($configs);`
|
|
||||||
3. AdminGoodsSaveHandle.php:148 — 写回前加 `if (!empty($configs))`
|
|
||||||
4. AdminGoodsSaveHandle.php:158-173 — BatchGenerate 前增加模板存在性显式校验
|
|
||||||
|
|
||||||
### P2 Fix(高优先级)
|
|
||||||
1. SeatSkuService.php GetGoodsViewData — 遍历所有有效配置块,不只处理 `$vrGoodsConfig[0]`
|
|
||||||
2. 修改 DB 写回逻辑为写回 `validConfigs` 而非 `[$config]`
|
|
||||||
|
|
||||||
### P3 Fix(中优先级)
|
|
||||||
1. AdminGoodsSave.php — configs 为空时提示用户重新选择场馆
|
|
||||||
Loading…
Reference in New Issue