vr-shopxo-plugin/reviews/SecurityEngineer-GHOST_SPEC...

233 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 安全审计报告:幽灵 SpecGhost 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` 字段无大小限制,可存储超大 JSONDoS 风险)— 需 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已售座位标记以提升用户体验和防止超卖。