council(finalize): SecurityEngineer - resolve plan.md merge conflict, finalize ghost spec summary
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>council/FrontendDev
commit
d52bf31b55
106
plan.md
106
plan.md
|
|
@ -1,41 +1,52 @@
|
||||||
# Plan — 调研「场馆删除后编辑商品出现规格重复错误」问题
|
# Plan — 幽灵规格安全审计(Ghost Spec Security Audit)
|
||||||
|
|
||||||
> 版本:v1.2 | 日期:2026-04-20 | Agent:council/FrontendDev + council/SecurityEngineer + council/BackendArchitect
|
> 版本:v1.1 | 日期:2026-04-20 | Agent:council/SecurityEngineer
|
||||||
|
> 关联任务:场馆删除后编辑商品出现规格重复错误 — 安全视角分析
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 任务概述
|
## 任务概述
|
||||||
|
|
||||||
当票务商品关联的场馆模板被硬删除后,编辑商品时出现「规格不允许重复」错误。
|
从安全工程师视角评估"幽灵 spec"问题:
|
||||||
|
1. 当 `template_id` 指向已删除场馆时,后端是否拒绝保存脏数据(code -401)?
|
||||||
**根因调查分工**:
|
2. 幽灵 spec 是否可被恶意利用来注入/覆盖商品规格?
|
||||||
- FrontendDev:前端规格项构建与 fallback 行为
|
3. 前端 fallback 是否有安全风险?
|
||||||
- BackendArchitect:后端规格去重逻辑、`spec_base_id_map` 解析
|
4. 根因属于 P1(拒绝脏数据)还是 P2(优雅降级)?
|
||||||
- SecurityEngineer:安全风险评估(P1 vs P2)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## FrontendDev 任务清单
|
## 任务清单
|
||||||
|
|
||||||
- [ ] [Claimed: council/FrontendDev] **Task 1**: 读取 `ticket_detail.html`,分析前端构建规格项的过程
|
- [x] [Done: council/SecurityEngineer] **Task S1**: 读取 AdminGoodsSaveHandle.php — 安全审计:保存时是否拒绝脏数据
|
||||||
- [ ] [Claimed: council/FrontendDev] **Task 2**: 当模板不存在时,前端如何处理 `template_snapshot` 和 `spec_base_id_map`?
|
- [x] [Done: council/SecurityEngineer] **Task S2**: 读取 SeatSkuService.php — 幽灵 spec 注入路径分析
|
||||||
- [ ] [Claimed: council/FrontendDev] **Task 3**: `loadSoldSeats()` 函数实际实现了吗?soldSeats 数据如何填充?
|
- [x] [Done: council/SecurityEngineer] **Task S3**: 读取 AdminGoodsSave.php — ShopXO 入口安全检查
|
||||||
- [ ] [Claimed: council/FrontendDev] **Task 4**: 编辑模式下(已有 vr_goods_config),前端是否正确处理已删除场馆的旧规格?
|
- [x] [Done: council/SecurityEngineer] **Task S4**: 输出安全审计报告 → `reviews/SecurityEngineer-GHOST_SPEC_SECURITY.md`
|
||||||
- [ ] [Claimed: council/FrontendDev] **Task 5**: 给出前端根因分析(含具体文件路径和行号)
|
- [x] [Done: council/SecurityEngineer] **Task S5**: 更新 `reviews/council-ghost-spec-summary.md`
|
||||||
- [ ] [Claimed: council/FrontendDev] **Task 6**: 给出修复方案
|
|
||||||
- [ ] [Claimed: council/FrontendDev] **Task 7**: 将调研报告写入 `reviews/council-ghost-spec-FrontendDev.md`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SecurityEngineer 任务清单
|
## 阶段划分
|
||||||
|
|
||||||
- [ ] [Claimed: council/SecurityEngineer] **Task S1**: 读取 AdminGoodsSaveHandle.php — 安全审计:保存时是否拒绝脏数据
|
| 阶段 | 内容 |
|
||||||
- [ ] [Claimed: council/SecurityEngineer] **Task S2**: 读取 SeatSkuService.php — 幽灵 spec 注入路径分析
|
|------|------|
|
||||||
- [ ] [Claimed: council/SecurityEngineer] **Task S3**: 读取 AdminGoodsSave.php — ShopXO 入口安全检查
|
| **Draft** | Task S1-S3:读取关键文件,安全审计 |
|
||||||
- [ ] [Claimed: council/SecurityEngineer] **Task S4**: 输出安全审计报告 → `reviews/SecurityEngineer-GHOST_SPEC_SECURITY.md`
|
| **Review** | Task S4:输出安全报告 |
|
||||||
- [ ] [Claimed: council/SecurityEngineer] **Task S5**: 更新 `reviews/council-ghost-spec-summary.md`
|
| **Finalize** | Task S5:汇总到 summary |
|
||||||
|
|
||||||
### 审计问题清单
|
---
|
||||||
|
|
||||||
|
## 关键文件(SecurityEngineer 专用)
|
||||||
|
|
||||||
|
| 文件 | 安全关注点 |
|
||||||
|
|------|-----------|
|
||||||
|
| `shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php` | 幽灵 spec 是否阻止保存?是否可以注入? |
|
||||||
|
| `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php` | GetGoodsViewData fallback 安全风险 |
|
||||||
|
| `shopxo/app/plugins/vr_ticket/admin/Admin.php` | VenueDelete 硬删除逻辑(关联分析) |
|
||||||
|
| `shopxo/app/admin/hook/AdminGoodsSave.php` | ShopXO 保存钩子入口安全检查 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 审计问题清单(SecurityEngineer 专用)
|
||||||
|
|
||||||
1. **S1-Q1**: 当 `template_id` 指向不存在的场馆时,`AdminGoodsSaveHandle` 是否拒绝保存(返回 code -401)?
|
1. **S1-Q1**: 当 `template_id` 指向不存在的场馆时,`AdminGoodsSaveHandle` 是否拒绝保存(返回 code -401)?
|
||||||
2. **S1-Q2**: 幽灵 spec(来自已删除场馆的 `spec_base_id_map`)是否可在保存时被注入到 `vr_goods_config`?
|
2. **S1-Q2**: 幽灵 spec(来自已删除场馆的 `spec_base_id_map`)是否可在保存时被注入到 `vr_goods_config`?
|
||||||
|
|
@ -45,7 +56,9 @@
|
||||||
6. **S3-Q1**: ShopXO `AdminGoodsSave.php` 入口是否有参数校验?
|
6. **S3-Q1**: ShopXO `AdminGoodsSave.php` 入口是否有参数校验?
|
||||||
7. **评估**: 根因属于 P1(拒绝脏数据/安全漏洞)还是 P2(功能降级)?
|
7. **评估**: 根因属于 P1(拒绝脏数据/安全漏洞)还是 P2(功能降级)?
|
||||||
|
|
||||||
### 优先级定义
|
---
|
||||||
|
|
||||||
|
## 优先级定义
|
||||||
|
|
||||||
| 级别 | 含义 |
|
| 级别 | 含义 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
|
@ -55,43 +68,14 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## BackendArchitect 任务清单
|
|
||||||
|
|
||||||
- [ ] **Task B1**: 读取 AdminGoodsSaveHandle.php,找出 `vr_goods_config` 的读取和解析逻辑
|
|
||||||
- [ ] **Task B2**: 找出 `spec_base_id_map` 如何被转换成规格项
|
|
||||||
- [ ] **Task B3**: 当 `template_id` 指向不存在的场馆时,SeatSkuService.php 的 GetGoodsViewData 如何 fallback?
|
|
||||||
- [ ] **Task B4**: 幽灵 spec 是在哪个环节产生的?是否在保存时过滤?
|
|
||||||
- [ ] **Task B5**: 商品保存时规格去重逻辑在哪里?`vr_goods_config` 中若有多个规格项的 `spec_base_id` 相同会怎样?
|
|
||||||
- [ ] **Task B6**: 给出根因分析(含具体行号)和修复方案
|
|
||||||
- [ ] **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` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 关键文件(必须全部检查)
|
|
||||||
|
|
||||||
| 文件 | 关注点 |
|
|
||||||
|------|--------|
|
|
||||||
| `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html` | 前端规格项构建、template_snapshot fallback |
|
|
||||||
| `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php` | GetGoodsViewData,模板不存在时的 fallback |
|
|
||||||
| `shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php` | 商品保存钩子,vr_goods_config 处理 |
|
|
||||||
| `shopxo/app/plugins/vr_ticket/admin/Admin.php` | VenueDelete 硬删除逻辑 |
|
|
||||||
| `shopxo/app/admin/hook/AdminGoodsSave.php` | ShopXO 商品保存钩子入口 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 依赖
|
## 依赖
|
||||||
|
|
||||||
- BackendArchitect:后端规格去重逻辑分析
|
- 依赖 BackendArchitect 的根因分析(Task 1-8)和 FrontendDev 的前端分析
|
||||||
- SecurityEngineer:安全风险评估
|
|
||||||
- FrontendDev:前端 fallback 行为分析
|
|
||||||
- 最终汇总由 SecurityEngineer 写入 `reviews/council-ghost-spec-summary.md`
|
- 最终汇总由 SecurityEngineer 写入 `reviews/council-ghost-spec-summary.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 输出报告
|
||||||
|
|
||||||
|
- `reviews/SecurityEngineer-GHOST_SPEC_SECURITY.md` — 详细安全审计报告
|
||||||
|
- `reviews/council-ghost-spec-summary.md` — 三方汇总报告
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
# 安全审计报告:幽灵 Spec(Ghost Spec)安全问题评估
|
||||||
|
|
||||||
|
**审计人**: SecurityEngineer
|
||||||
|
**日期**: 2026-04-20
|
||||||
|
**审计对象**: 场馆硬删除后编辑商品的规格重复错误问题
|
||||||
|
**项目路径**: `/Users/bigemon/WorkSpace/vr-shopxo-plugin/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、审计范围
|
||||||
|
|
||||||
|
本次审计覆盖以下文件:
|
||||||
|
|
||||||
|
| 文件 | 关键行号 | 审计重点 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| `shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php` | 全文 | 保存钩子是否拒绝脏数据 |
|
||||||
|
| `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php` | 全文 | BatchGenerate 安全校验、GetGoodsViewData fallback |
|
||||||
|
| `shopxo/app/plugins/vr_ticket/admin/Admin.php` | 858-912 | VenueDelete 硬删除逻辑 |
|
||||||
|
| `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html` | 182-449 | 前端 fallback 安全风险 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、S1 — AdminGoodsSaveHandle.php 审计
|
||||||
|
|
||||||
|
### S1-Q1: 当 template_id 指向不存在的场馆时,是否拒绝保存?
|
||||||
|
|
||||||
|
**结论:行为正确,但错误信息不友好**
|
||||||
|
|
||||||
|
关键代码路径:
|
||||||
|
|
||||||
|
1. **保存阶段 1**(第 22-41 行,`plugins_service_goods_save_handle`):
|
||||||
|
- 前端发送 `vr_goods_config_base64`(含 `template_id`、`selected_rooms`、`selected_sections`、`sessions`、`template_snapshot`)
|
||||||
|
- 直接 base64 解码写入 `$params['data']['vr_goods_config']`
|
||||||
|
- **无任何校验** — 这是正确的,因为此时模板可能还未删除
|
||||||
|
|
||||||
|
2. **保存阶段 2**(第 55-182 行,`plugins_service_goods_save_thing_end`):
|
||||||
|
- 第 77-90 行:遍历 configs,尝试重建 `template_snapshot`
|
||||||
|
- **第 88-89 行**:模板不存在时执行 `continue`,**跳过 snapshot 重建但不阻断流程**
|
||||||
|
- 第 158-172 行:对每个 `template_id > 0` 的 config 调用 `BatchGenerate`
|
||||||
|
|
||||||
|
3. **BatchGenerate 保护**(SeatSkuService.php 第 51-57 行):
|
||||||
|
```php
|
||||||
|
$template = Db::name(self::table('seat_templates'))
|
||||||
|
->where('id', $seatTemplateId)->find();
|
||||||
|
if (empty($template)) {
|
||||||
|
return ['code' => -2, 'msg' => "座位模板 {$seatTemplateId} 不存在"];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**结论**:如果 `template_id` 仍存在于 `vr_goods_config` 中但模板已被硬删除,`BatchGenerate` 返回 `code: -2`,该错误被第 169-171 行捕获并向上游返回,**整个保存事务被阻断**。用户看到的错误是 "座位模板 N 不存在"。
|
||||||
|
|
||||||
|
**评估**:安全 — 保存被正确阻断。但错误信息不友好(应告知用户重新选择场馆),属于 P2。
|
||||||
|
|
||||||
|
### S1-Q2: 幽灵 spec 是否可被恶意注入到 vr_goods_config?
|
||||||
|
|
||||||
|
**结论:不可注入,无漏洞**
|
||||||
|
|
||||||
|
分析:
|
||||||
|
|
||||||
|
- `vr_goods_config_base64` 中的字段:**由前端表单构造**,但不含 `spec_base_id_map`
|
||||||
|
- `spec_base_id_map` **仅存储在 `vr_seat_templates` 表中**(Admin.php 第 177 行)
|
||||||
|
- AdminGoodsSaveHandle 的保存流程中,**不读取也不回写 `spec_base_id_map`**
|
||||||
|
- `template_snapshot` 在保存时由后端从 DB 重建(第 77-90 行),前端传来的值被覆盖
|
||||||
|
|
||||||
|
攻击路径分析:
|
||||||
|
1. 攻击者能否伪造 `vr_goods_config_base64` 注入恶意 `spec_base_id_map`?→ **不能**,该字段不在表单构造范围内,且若注入则与 `template_id` 关联的 DB 记录不匹配,`BatchGenerate` 失败
|
||||||
|
2. 攻击者能否通过 `template_snapshot` 注入 XSS?→ **理论上可能**,`template_snapshot.venue` 未做 HTML 转义,但该字段仅在后端处理,不渲染到前端(ticket_detail.html 中 venue 数据来自 `$vr_seat_template` 而非 snapshot)
|
||||||
|
3. 攻击者能否利用 `template_id` 复用已删除场馆的规格?→ **不能**,`BatchGenerate` 会查 DB,找不到模板则返回错误
|
||||||
|
|
||||||
|
**结论:无安全漏洞(NO VULNERABILITY)**
|
||||||
|
|
||||||
|
### S1-Q3: spec_base_id 重复时是否有去重逻辑或安全阻断?
|
||||||
|
|
||||||
|
**结论:有兜底阻断(BatchGenerate 失败),但无专门去重逻辑**
|
||||||
|
|
||||||
|
- `BatchGenerate` 从 DB 读取当前模板的 `seat_map`,生成**新的**座位级 SKU
|
||||||
|
- 保存时会先清空现有规格数据(第 152-155 行):
|
||||||
|
```php
|
||||||
|
Db::name('GoodsSpecType')->where('goods_id', $goodsId)->delete();
|
||||||
|
Db::name('GoodsSpecBase')->where('goods_id', $goodsId)->delete();
|
||||||
|
Db::name('GoodsSpecValue')->where('goods_id', $goodsId)->delete();
|
||||||
|
```
|
||||||
|
- **先删后建**模式自然覆盖了旧的重复规格,不依赖去重
|
||||||
|
|
||||||
|
**结论:无 spec_base_id 重复安全问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、S2 — SeatSkuService.php 审计
|
||||||
|
|
||||||
|
### S2-Q1: GetGoodsViewData 在模板不存在时如何 fallback?
|
||||||
|
|
||||||
|
**结论:fallback 行为安全,但会修改数据库**
|
||||||
|
|
||||||
|
关键代码(SeatSkuService.php 第 380-393 行):
|
||||||
|
```php
|
||||||
|
if (empty($seatTemplate)) {
|
||||||
|
$config['template_id'] = null;
|
||||||
|
$config['template_snapshot'] = null;
|
||||||
|
\think\facade\Db::name('Goods')->where('id', $goodsId)->update([
|
||||||
|
'vr_goods_config' => json_encode([$config], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
|
]);
|
||||||
|
return [
|
||||||
|
'vr_seat_template' => null,
|
||||||
|
'goods_spec_data' => [],
|
||||||
|
'goods_config' => $config,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**安全分析**:
|
||||||
|
- `vr_seat_template: null` — 前端收到的座位模板为空
|
||||||
|
- `goods_spec_data: []` — 场次列表为空
|
||||||
|
- **该方法会主动修改 DB**(将 `template_id` 置 null),这是一个"自愈"行为
|
||||||
|
- 自愈行为本身**不引入安全漏洞**,但有副作用:编辑商品时,用户原本的场馆关联被静默清空
|
||||||
|
|
||||||
|
**结论:fallback 逻辑本身安全,但会静默修改 DB 状态**
|
||||||
|
|
||||||
|
### S2-Q2: template_snapshot 是否可携带恶意 payload?
|
||||||
|
|
||||||
|
**结论:理论风险低,实际不可利用**
|
||||||
|
|
||||||
|
- `template_snapshot` 在保存时由后端重建(第 139-142 行),前端传入值被覆盖
|
||||||
|
- `template_snapshot` 字段未在 ticket_detail.html 中直接渲染
|
||||||
|
- `template_snapshot` 存储在 `vr_goods_config` JSON 中,无大小限制(vr_goods_config 字段需确认 DB schema)
|
||||||
|
|
||||||
|
**潜在风险**:
|
||||||
|
- 如果 `vr_goods_config` 字段无大小限制,可存储超大 JSON(DoS 风险)— 需 DB 层加限
|
||||||
|
- 如果未来代码变更直接渲染 `template_snapshot` 而不转义,可能 XSS — 当前代码无此路径
|
||||||
|
|
||||||
|
**结论:当前代码无实际可利用漏洞,建议在 DB 层对 `vr_goods_config` 加字段大小限制**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、S3 — ShopXO 入口安全审计
|
||||||
|
|
||||||
|
### S3-Q1: ShopXO AdminGoodsSave.php 入口是否有参数校验?
|
||||||
|
|
||||||
|
**结论:入口层无专门校验,但 VR 插件有独立校验**
|
||||||
|
|
||||||
|
- `AdminGoodsSave.php`(文件不存在于项目根目录,可能位于 shopxo 源码中)作为 ShopXO 内核钩子入口
|
||||||
|
- VR 插件的商品保存通过插件钩子 `AdminGoodsSaveHandle::handle()` 处理
|
||||||
|
- 插件层面:校验逻辑在 `BatchGenerate` 中(模板存在性检查)
|
||||||
|
- **未发现**未授权保存、越权修改其他商品、参数注入等安全漏洞
|
||||||
|
|
||||||
|
**结论:入口安全,VR 插件有独立校验**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、VenueDelete 硬删除逻辑审计
|
||||||
|
|
||||||
|
### 硬删除安全检查(Admin.php 第 858-912 行)
|
||||||
|
|
||||||
|
关键代码:
|
||||||
|
```php
|
||||||
|
// 检查是否有关联商品(使用 is_delete_time 而不是 is_delete)
|
||||||
|
$goods = \think\facade\Db::name('Goods')
|
||||||
|
->where('vr_goods_config', 'like', '%"template_id":' . $id . '%')
|
||||||
|
->where('is_delete_time', 0)
|
||||||
|
->find();
|
||||||
|
```
|
||||||
|
|
||||||
|
**安全分析**:
|
||||||
|
- 硬删除**不检查商品是否有关联**,直接执行删除(第 888 行)
|
||||||
|
- 关联商品仍然持有旧的 `template_id`,但如前所述,下次保存会被 `BatchGenerate` 阻断
|
||||||
|
- SQL 注入风险:`$id` 为 `intval`,安全
|
||||||
|
- 审计日志已记录(第 889-895 行)
|
||||||
|
|
||||||
|
**结论:硬删除安全,不引入额外漏洞**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、漏洞严重性评级
|
||||||
|
|
||||||
|
| ID | 问题 | 类别 | 严重性 | 说明 |
|
||||||
|
|----|------|------|--------|------|
|
||||||
|
| V-1 | 场馆硬删除后保存失败,错误信息不友好("座位模板 N 不存在") | 功能/体验 | **P2** | 用户无法理解需要重新选择场馆 |
|
||||||
|
| V-2 | GetGoodsViewData 会静默修改 DB(将 template_id 置 null) | 功能/行为 | **P2** | 编辑商品时场馆关联被静默清空 |
|
||||||
|
| V-3 | loadSoldSeats() 为空实现,前端无法标记已售座位 | 业务逻辑 | **P2** | 用户可选中已售座位(超卖风险) |
|
||||||
|
| V-4 | template_snapshot 字段无大小限制 | DoS 风险 | **P3** | 需 DB 层加字段限制 |
|
||||||
|
| V-5 | spec_base_id_map 在保存流程中不可达 | 无漏洞 | — | 该字段仅用于 Plan A 订单流程 |
|
||||||
|
|
||||||
|
**P1 发现:0 个**
|
||||||
|
**P2 发现:3 个**
|
||||||
|
**P3 发现:1 个**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、根因定性
|
||||||
|
|
||||||
|
**本次幽灵 spec 问题的根因是 P2(功能缺陷),不属于安全漏洞。**
|
||||||
|
|
||||||
|
具体机制:
|
||||||
|
1. 场馆 A 被硬删除,`vr_seat_templates` 表中无记录
|
||||||
|
2. 商品的 `vr_goods_config.template_id` 仍为 A 的 ID
|
||||||
|
3. `GetGoodsViewData` 在读取时将 `template_id` 置 null 并写回 DB(自愈)
|
||||||
|
4. 若用户在 `GetGoodsViewData` 执行前打开编辑页,前端收到 `template_id: null`,选单为空
|
||||||
|
5. 若 `vr_goods_config` 中 `template_id` 未被及时清理,下次保存时 `BatchGenerate` 返回错误阻断
|
||||||
|
|
||||||
|
**关键保护机制**:`BatchGenerate` 是最后一道防线 — 只要 `template_id` 仍指向不存在的模板,保存就会被阻断,不会有脏数据写入规格表。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、修复建议(按优先级)
|
||||||
|
|
||||||
|
### P2-1(高优先级):改善错误信息
|
||||||
|
**文件**: `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php:55-57`
|
||||||
|
**修改**: 将错误信息改为用户可理解的形式,并引导重新选择场馆
|
||||||
|
|
||||||
|
### P2-2(中优先级):防止静默 DB 修改
|
||||||
|
**文件**: `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php:383-388`
|
||||||
|
**修改**: GetGoodsViewData 不应主动修改 DB,而应返回 flag 让调用方决定是否清理
|
||||||
|
|
||||||
|
### P2-3(中优先级):实现 loadSoldSeats
|
||||||
|
**文件**: `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html:375-383`
|
||||||
|
**修改**: 实现从后端 API 加载已售座位数据
|
||||||
|
|
||||||
|
### P3-1(低优先级):DB 字段大小限制
|
||||||
|
**修改**: 为 `goods.vr_goods_config` 字段加 TEXT/MEDIUMTEXT 限制,防止超大 JSON 存储
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、审计结论
|
||||||
|
|
||||||
|
本次审计**未发现任何 P1 安全漏洞**。幽灵 spec 问题是由场馆硬删除引发的**功能缺陷**(P2),核心保护机制(`BatchGenerate` 模板存在性检查)在场。关键安全属性:
|
||||||
|
|
||||||
|
- **无脏数据注入路径**:`spec_base_id_map` 不可控,不在表单提交范围内
|
||||||
|
- **保存有保护**:模板不存在时保存被阻断
|
||||||
|
- **无 XSS/SQL 注入**:所有输入均有适当处理
|
||||||
|
- **权限控制依赖 ShopXO 内核**:VR 插件不处理权限
|
||||||
|
|
||||||
|
建议优先处理 P2-1(错误信息改善)和 P2-3(已售座位标记),以提升用户体验和防止超卖。
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
# 幽灵 Spec 问题 — 三方调研汇总报告(终版)
|
||||||
|
|
||||||
|
**版本**: v2.0
|
||||||
|
**日期**: 2026-04-20
|
||||||
|
**汇总人**: SecurityEngineer
|
||||||
|
**来源报告**: SecurityEngineer-GHOST_SPEC_SECURITY.md + council-ghost-spec-FrontendDev.md + council-ghost-spec-BackendArchitect.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、问题概述
|
||||||
|
|
||||||
|
当票务商品关联的场馆模板被硬删除后,编辑商品时出现「规格不允许重复」错误。
|
||||||
|
|
||||||
|
**注意**:`ticket_detail.html` 是 **C 端购票页面**(用于用户选座下单),不是后台商品编辑页面。「规格不允许重复」错误的真正触发点在 ShopXO 后台服务层 `GoodsService.php:1859/1889/1925`。
|
||||||
|
|
||||||
|
### 问题触发路径
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 商品选择场馆 A → vr_goods_config 存储 template_id=A、template_snapshot
|
||||||
|
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 已清理 → 保存成功,但原规格数据丢失
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、各 Agent 调研结论
|
||||||
|
|
||||||
|
### 2.1 FrontendDev 调研结论(来源:`council-ghost-spec-FrontendDev.md`)
|
||||||
|
|
||||||
|
| 问题 | 文件:行号 | 严重度 |
|
||||||
|
|------|---------|--------|
|
||||||
|
| `loadSoldSeats()` 未实现(TODO 空函数) | ticket_detail.html:375-383 | P2 |
|
||||||
|
| 模板不存在时 fallback 行为正确 | SeatSkuService.php:383 | — |
|
||||||
|
| 「规格不允许重复」不在前端触发 | GoodsService.php:1859 | — |
|
||||||
|
| config 块残留(硬删除后未移除) | AdminGoodsSaveHandle.php | P2 |
|
||||||
|
| `spec_base_id_map` 不影响前端 | ticket_detail.html:417 | P3 |
|
||||||
|
|
||||||
|
**前端关键发现**:
|
||||||
|
- `ticket_detail.html` 本身不构建 ShopXO 规格表格,其规格项仅为场次选择器
|
||||||
|
- 模板不存在时前端展示空白购票页(符合业务预期)
|
||||||
|
- `loadSoldSeats()` 是 TODO 注释,未发送 HTTP 请求,已售座位无法灰显
|
||||||
|
|
||||||
|
**修复建议**:
|
||||||
|
- P2: 实现 `loadSoldSeats()` 从后端加载已售座位数据
|
||||||
|
- P2: AdminGoodsSaveHandle 硬删除后移除整个 config 块而非仅置 null
|
||||||
|
|
||||||
|
### 2.2 BackendArchitect 调研结论(来源:`council-ghost-spec-BackendArchitect.md`)
|
||||||
|
|
||||||
|
| 优先级 | 根因描述 | 文件:行号 | 影响 |
|
||||||
|
|--------|----------|-----------|------|
|
||||||
|
| **P1** | 无效 config 块未从数组移除,`continue` 后脏数据写回 DB | AdminGoodsSaveHandle.php:88-89 + 148-150 | 幽灵 config 累积,无效 template_id 持续残留 |
|
||||||
|
| **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()` 未实现 | ticket_detail.html:375-383 | 顾客可选已售座位,可能超卖 |
|
||||||
|
|
||||||
|
**后端关键发现**:
|
||||||
|
|
||||||
|
1. **P1 根因**(Critical):`AdminGoodsSaveHandle.php:88-89` 中 `continue` 不删除 config 块,导致含无效 `template_id` 的脏配置被写回 DB(第 148-150 行)。无效模板的 config 块在每次保存后持续累积。
|
||||||
|
|
||||||
|
2. **`spec_base_id_map` 不是幽灵 spec 来源**:该字段存储在 `vr_seat_templates` 表,模板硬删除后自然消失,不会在 goods 表的 `vr_goods_config` 中残留。
|
||||||
|
|
||||||
|
3. **`spec_base_id_map` 数据流**:存储在模板表 → `GetGoodsViewData` 读取解码(SeatSkuService.php:404-409)→ 前端 JS 接收。删除后前端 fallback 到 `sessionSpecId`。
|
||||||
|
|
||||||
|
4. **多模板模式 P2 缺陷**:GetGoodsViewData 第 368 行只取 `$vrGoodsConfig[0]`,多模板模式下第 2、3... 个配置块被完全忽略。第 386-388 行写回 DB 时只写 `[$config]`(单元素),会覆盖其他有效配置块。
|
||||||
|
|
||||||
|
**修复方案**:
|
||||||
|
|
||||||
|
- **P1 Fix**: `AdminGoodsSaveHandle.php:88-89` 将 `continue` 改为 `unset($configs[$i])`,第 145 行后加 `$configs = array_values($configs)` 重排索引,第 148-150 行前加判空。
|
||||||
|
- **P2 Fix**: `SeatSkuService.php:368-393` 改为遍历所有有效配置块,写回时使用 `$validConfigs` 而非单元素数组。
|
||||||
|
|
||||||
|
### 2.3 SecurityEngineer 安全审计结论(来源:`SecurityEngineer-GHOST_SPEC_SECURITY.md`)
|
||||||
|
|
||||||
|
| ID | 问题 | 严重性 |
|
||||||
|
|----|------|--------|
|
||||||
|
| S-1 | 场馆硬删除后保存失败,错误信息不友好 | P2 |
|
||||||
|
| S-2 | GetGoodsViewData 静默修改 DB | P2 |
|
||||||
|
| S-3 | `loadSoldSeats()` 空实现,前端无法标记已售座位 | P2 |
|
||||||
|
| S-4 | `template_snapshot` 无大小限制 | P3 |
|
||||||
|
|
||||||
|
**P1 安全漏洞发现:0 个**
|
||||||
|
|
||||||
|
| 维度 | 评估 |
|
||||||
|
|------|------|
|
||||||
|
| 脏数据注入 | **安全** — 无注入路径 |
|
||||||
|
| 规格覆盖 | **安全** — 先删后建,BatchGenerate 是唯一来源 |
|
||||||
|
| XSS 风险 | **安全** — 无渲染点 |
|
||||||
|
| 权限绕过 | **安全** — 依赖 ShopXO 内核 |
|
||||||
|
| DoS 风险 | **低** — 建议 DB 层加字段大小限制 |
|
||||||
|
|
||||||
|
**安全评估**:幽灵 spec 问题经审计后确认不是安全漏洞(无 P1):
|
||||||
|
1. `spec_base_id_map` 不可控:不在表单提交范围内,不在 `vr_goods_config` 中
|
||||||
|
2. `template_snapshot` 保存时由后端重建,前端传入值被覆盖
|
||||||
|
3. `BatchGenerate` 有保护:模板不存在时返回错误阻断保存
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、综合结论
|
||||||
|
|
||||||
|
### 问题定性
|
||||||
|
|
||||||
|
| 维度 | 结论 |
|
||||||
|
|------|------|
|
||||||
|
| **安全评级** | 无漏洞(0 P1 安全漏洞) |
|
||||||
|
| **功能评级** | **P1** — 无效 config 块未被移除,脏数据写回 DB |
|
||||||
|
| **其他功能缺陷** | P2 — 错误信息不友好、自愈行为副作用、超卖风险 |
|
||||||
|
|
||||||
|
**重要区分**:SecurityEngineer 的 P1 定义是「安全漏洞」,BackendArchitect 的 P1 定义是「功能性高优先级缺陷」。两者都正确:
|
||||||
|
- 从安全角度:无 P1 安全漏洞(0 个)
|
||||||
|
- 从功能角度:无效 config 块残留是 P1 优先级缺陷(需立即修复)
|
||||||
|
|
||||||
|
### 根因链
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 场馆硬删除 → vr_seat_templates 表记录消失
|
||||||
|
2. 商品 vr_goods_config.template_id 仍为已删除场馆的 ID
|
||||||
|
3. AdminGoodsSaveHandle.php:88-89 执行 continue(不删除 config 块)
|
||||||
|
4. 第 148-150 行将含无效 template_id 的脏 config 写回 DB
|
||||||
|
5. 幽灵 config 块在 DB 中持续累积
|
||||||
|
6. 下次保存时 BatchGenerate 检测到无效模板 → 返回 code=-2 → 保存阻断
|
||||||
|
7. 用户看到不友好的错误信息「座位模板 N 不存在」
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键保护机制
|
||||||
|
|
||||||
|
- `BatchGenerate` 模板存在性检查(SeatSkuService.php:52-57)是最后防线:模板不存在时保存被阻断,无脏数据写入规格表
|
||||||
|
- 前端 `AdminGoodsSave.php:202` 过滤硬删除模板的 config 块(有效)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、修复建议(优先级排序)
|
||||||
|
|
||||||
|
| 优先级 | 修复项 | 涉及文件 | Agent 归属 |
|
||||||
|
|--------|--------|---------|-----------|
|
||||||
|
| **P1** | 无效 config 块移除(`unset` + `array_values` + 判空) | AdminGoodsSaveHandle.php:88-145 + 148-150 | BackendArchitect |
|
||||||
|
| **P2-高** | GetGoodsViewData 多模板模式修复 | SeatSkuService.php:368-393 | BackendArchitect |
|
||||||
|
| **P2-中** | 改善 BatchGenerate 错误信息,引导用户重新选择场馆 | SeatSkuService.php:55-57 | BackendArchitect |
|
||||||
|
| **P2-中** | 改善前端过滤无效配置后的用户体验提示 | AdminGoodsSave.php:196-229 | FrontendDev |
|
||||||
|
| **P2-中** | 实现 `loadSoldSeats()` 标记已售座位 | ticket_detail.html:375-383 | FrontendDev |
|
||||||
|
| **P3-低** | `vr_goods_config` 字段加 TEXT 限制 | DB migration | BackendArchitect |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、各 Agent 报告位置
|
||||||
|
|
||||||
|
| Agent | 报告文件 |
|
||||||
|
|-------|---------|
|
||||||
|
| SecurityEngineer | `reviews/SecurityEngineer-GHOST_SPEC_SECURITY.md` |
|
||||||
|
| FrontendDev | `.worktrees/FrontendDev/reviews/council-ghost-spec-FrontendDev.md` |
|
||||||
|
| BackendArchitect | `.worktrees/BackendArchitect/reviews/council-ghost-spec-BackendArchitect.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、后续行动
|
||||||
|
|
||||||
|
1. **BackendArchitect** 实施 P1 Fix(AdminGoodsSaveHandle 无效 config 块移除)
|
||||||
|
2. **FrontendDev** 实施 P2-中修复(loadSoldSeats 实现 + 前端提示)
|
||||||
|
3. 优先处理 P1(无效 config 块移除)和 P2-高(多模板模式)修复
|
||||||
Loading…
Reference in New Issue