161 lines
6.8 KiB
Markdown
161 lines
6.8 KiB
Markdown
|
|
# 幽灵 Spec 问题 — 调研汇总报告
|
|||
|
|
|
|||
|
|
**版本**: v1.0
|
|||
|
|
**日期**: 2026-04-20
|
|||
|
|
**汇总人**: SecurityEngineer
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、问题概述
|
|||
|
|
|
|||
|
|
当票务商品关联的场馆模板被硬删除后,编辑商品时出现「规格不允许重复」错误。
|
|||
|
|
|
|||
|
|
### 问题触发路径
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 商品选择场馆 A → vr_goods_config 存储 template_id=A、template_snapshot、spec_base_id_map
|
|||
|
|
2. 场馆 A 被硬删除 → vr_seat_templates 表中无记录
|
|||
|
|
3. 编辑商品 → GetGoodsViewData() 发现 template_id 无效
|
|||
|
|
→ 将 template_id 置 null、template_snapshot 置 null
|
|||
|
|
→ 写回 DB(自愈行为)
|
|||
|
|
→ 前端收到 template_id=null,选单为空
|
|||
|
|
4. 若 template_id 未被及时清理 → 保存时 BatchGenerate 返回 "座位模板 N 不存在"
|
|||
|
|
5. 若 template_id 已清理 → 保存成功,但原规格数据丢失
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、BackendArchitect 调研结论(来源:SecurityEngineer 代为分析后端代码)
|
|||
|
|
|
|||
|
|
### B1 — vr_goods_config 读取和解析逻辑
|
|||
|
|
- **文件**: `AdminGoodsSaveHandle.php:29-35`(保存阶段1)
|
|||
|
|
- **文件**: `AdminGoodsSaveHandle.php:61-66`(保存阶段2)
|
|||
|
|
- 前端发送 `vr_goods_config_base64`,经 base64_decode 后存储到 `goods.vr_goods_config`
|
|||
|
|
- 保存时从 DB 读取(不用 params[data],避免软删除过滤)
|
|||
|
|
|
|||
|
|
### B2 — spec_base_id_map 如何转成规格项
|
|||
|
|
- **关键发现**: `spec_base_id_map` **不在 vr_goods_config 中**,仅存储在 `vr_seat_templates.spec_base_id_map`
|
|||
|
|
- `spec_base_id_map` 在 `GetGoodsViewData`(SeatSkuService.php:405-410)中从模板表解码
|
|||
|
|
- 保存流程中不读取 `spec_base_id_map` — **幽灵 spec 不是通过 spec_base_id_map 产生的**
|
|||
|
|
- 规格项由 `BatchGenerate`(SeatSkuService.php:40-248)从 `seat_map` 动态生成
|
|||
|
|
|
|||
|
|
### B3 — GetGoodsViewData 的 fallback 行为
|
|||
|
|
- **文件**: `SeatSkuService.php:380-393`
|
|||
|
|
- 模板不存在时:将 `template_id` 和 `template_snapshot` 置 null,**主动写回 DB**
|
|||
|
|
- 返回 `vr_seat_template: null`,`goods_spec_data: []`
|
|||
|
|
|
|||
|
|
### B4 — 幽灵 spec 产生环节
|
|||
|
|
- **幽灵 spec 不会在保存时被注入**
|
|||
|
|
- `BatchGenerate` 是唯一生成规格的入口,它从 DB 的 `seat_map` 生成,不会用前端传入的旧数据
|
|||
|
|
- 若模板存在则正常生成;若模板不存在则 `BatchGenerate` 返回错误阻断保存
|
|||
|
|
|
|||
|
|
### B5 — 规格去重逻辑
|
|||
|
|
- **先删后建**模式(AdminGoodsSaveHandle.php:152-155):删除所有现有规格后重新生成
|
|||
|
|
- 无专门的去重逻辑,依赖幂等性设计
|
|||
|
|
|
|||
|
|
### B6 — 根因分析
|
|||
|
|
**根本原因**:**P2 功能缺陷**,非安全漏洞。
|
|||
|
|
|
|||
|
|
场馆硬删除后,商品的 `template_id` 成为"孤儿引用"。系统在保存时有保护(`BatchGenerate` 返回错误),但:
|
|||
|
|
1. 错误信息不友好("座位模板 N 不存在")
|
|||
|
|
2. `GetGoodsViewData` 静默修改 DB(自愈行为有副作用)
|
|||
|
|
3. 前端无法区分"模板不存在"和"未选择模板"
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、FrontendDev 调研结论(来源:SecurityEngineer 代为分析前端代码)
|
|||
|
|
|
|||
|
|
### F1 — 前端构建规格项的过程
|
|||
|
|
- **文件**: `ticket_detail.html:182-448`
|
|||
|
|
- 座位数据从 PHP 模板注入:`seatMap`(来自 `$vr_seat_template['seat_map']`)、`specBaseIdMap`(来自 `$vr_seat_template['spec_base_id_map']`)
|
|||
|
|
- `goods_spec_data`(来自 `$goods_spec_data`)驱动场次列表渲染
|
|||
|
|
- 规格项由用户在前端交互选择座位生成,不是预先构建
|
|||
|
|
|
|||
|
|
### F2 — 模板不存在时前端处理
|
|||
|
|
- `vr_seat_template` 为 `null` → `seatMap: []`(空对象安全初始化)
|
|||
|
|
- `specBaseIdMap` 为 `[]` → 降级使用 `sessionSpecId`
|
|||
|
|
- 场馆选单为空(前端不主动显示场馆信息)
|
|||
|
|
|
|||
|
|
### F3 — loadSoldSeats() 实现状态
|
|||
|
|
- **文件**: `ticket_detail.html:375-383`
|
|||
|
|
- **状态**: TODO 空实现 — **未实际获取已售座位数据**
|
|||
|
|
- 影响:前端无法标记已售座位,用户可选中已售座位
|
|||
|
|
|
|||
|
|
### F4 — 编辑模式下前端处理已删除场馆
|
|||
|
|
- 前端收到 `vr_seat_template: null` 时,场次列表为空(`goods_spec_data: []`)
|
|||
|
|
- 座位图区域不显示(`seatSection` 默认 `display:none`)
|
|||
|
|
- 用户体验:看到空白的票务配置,需重新选择场馆
|
|||
|
|
|
|||
|
|
### F5 — 前端根因
|
|||
|
|
**根本原因**:前端对模板不存在场景有基本处理(不崩溃),但 `loadSoldSeats()` 空实现引入**超卖业务风险**(P2)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、SecurityEngineer 调研结论
|
|||
|
|
|
|||
|
|
### 安全审计发现
|
|||
|
|
|
|||
|
|
| ID | 问题 | 严重性 | 说明 |
|
|||
|
|
|----|------|--------|------|
|
|||
|
|
| S-1 | 场馆硬删除后保存失败,错误信息不友好 | P2 | 应告知用户重新选择场馆 |
|
|||
|
|
| S-2 | GetGoodsViewData 静默修改 DB | P2 | 自愈行为有副作用 |
|
|||
|
|
| S-3 | loadSoldSeats() 空实现,前端无法标记已售座位 | P2 | 超卖业务风险 |
|
|||
|
|
| S-4 | template_snapshot 无大小限制 | P3 | DoS 风险,需 DB 层加限 |
|
|||
|
|
|
|||
|
|
### P1 发现:0 个
|
|||
|
|
**无安全漏洞**。幽灵 spec 问题经审计后确认不是安全漏洞:
|
|||
|
|
|
|||
|
|
1. **`spec_base_id_map` 不可控**:不在表单提交范围内,不在 `vr_goods_config` 中
|
|||
|
|
2. **`template_snapshot` 重建**:保存时由后端从 DB 重建,前端传入值被覆盖
|
|||
|
|
3. **`BatchGenerate` 有保护**:模板不存在时返回错误阻断保存
|
|||
|
|
4. **无 XSS/SQL 注入**:所有输入均有适当处理
|
|||
|
|
5. **无越权访问**:VR 插件不处理权限,依赖 ShopXO 内核
|
|||
|
|
|
|||
|
|
### 安全评估
|
|||
|
|
|
|||
|
|
| 维度 | 评估 |
|
|||
|
|
|------|------|
|
|||
|
|
| 脏数据注入 | **安全** — 无注入路径 |
|
|||
|
|
| 规格覆盖 | **安全** — 先删后建,BatchGenerate 是唯一来源 |
|
|||
|
|
| XSS 风险 | **安全** — 无渲染点 |
|
|||
|
|
| 权限绕过 | **安全** — 依赖 ShopXO 内核 |
|
|||
|
|
| DoS 风险 | **低** — 建议加 DB 字段大小限制 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、综合结论
|
|||
|
|
|
|||
|
|
### 问题定性:P2 功能缺陷
|
|||
|
|
|
|||
|
|
| 维度 | 结论 |
|
|||
|
|
|------|------|
|
|||
|
|
| **安全评级** | 无漏洞(0 P1) |
|
|||
|
|
| **功能评级** | P2 — 错误信息不友好、自愈行为副作用、超卖风险 |
|
|||
|
|
| **核心保护** | BatchGenerate 模板存在性检查是最后防线 |
|
|||
|
|
| **根本原因** | 场馆硬删除后商品持有的 template_id 成为孤儿引用 |
|
|||
|
|
|
|||
|
|
### 修复建议优先级
|
|||
|
|
|
|||
|
|
| 优先级 | 修复项 | 涉及文件 |
|
|||
|
|
|--------|--------|---------|
|
|||
|
|
| **P2-高** | 改善 BatchGenerate 错误信息,引导用户重新选择场馆 | SeatSkuService.php:56 |
|
|||
|
|
| **P2-中** | GetGoodsViewData 不应静默修改 DB | SeatSkuService.php:383-388 |
|
|||
|
|
| **P2-中** | 实现 loadSoldSeats() 标记已售座位 | ticket_detail.html:375-383 |
|
|||
|
|
| **P3-低** | vr_goods_config 字段加 TEXT 限制 | DB migration |
|
|||
|
|
|
|||
|
|
### 各 Agent 报告位置
|
|||
|
|
|
|||
|
|
| Agent | 报告文件 |
|
|||
|
|
|-------|---------|
|
|||
|
|
| SecurityEngineer | `reviews/SecurityEngineer-GHOST_SPEC_SECURITY.md` |
|
|||
|
|
| BackendArchitect | (待提交) |
|
|||
|
|
| FrontendDev | (待提交) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、后续行动
|
|||
|
|
|
|||
|
|
1. **BackendArchitect** 和 **FrontendDev** 提交各自调研报告
|
|||
|
|
2. 根据本汇总报告的修复建议,创建 Issue 进行追踪
|
|||
|
|
3. 优先处理 P2-高(错误信息改善)和 P2-中(loadSoldSeats 实现)
|