vr-shopxo-plugin/reviews/SecurityEngineer-on-GhostSp...

134 lines
5.3 KiB
Markdown
Raw Permalink Normal View History

# SecurityEngineer — 幽灵 spec 安全审计汇总报告
**文件路径基于** `/Users/bigemon/WorkSpace/vr-shopxo-plugin/`
**审计时间**2026-04-20
**参与者**SecurityEngineer安全审计、BackendArchitect根因分析、FrontendDev前端分析
---
## 执行摘要
对「场馆删除后编辑商品出现规格重复错误」问题进行了三方安全审计。核心根因已定位,**P1 安全缺陷**已识别。
---
## 审计范围
| 文件 | 用途 |
|------|------|
| `shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php` | 商品保存钩子vr_goods_config 处理 |
| `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php` | 批量 SKU 生成,模板不存在时的 fallback |
| `shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html` | 顾客端座位选购页面 |
| `shopxo/app/plugins/vr_ticket/admin/Admin.php` | 场馆硬删除逻辑 |
| `shopxo/app/admin/hook/AdminGoodsSave.php` | ShopXO 商品保存钩子入口 |
---
## 根因分析SecurityEngineer
### 根因 1P1无效 template_id 配置块未被过滤
**文件**`AdminGoodsSaveHandle.php:148-173`
`vr_goods_config` 中存在 `template_id` 指向已删除场馆的配置块时:
1. `save_thing_end` 从 DB 加载 config第 61-66 行)
2. 遍历 configs 尝试重建 `template_snapshot`(第 77 行)
3. 若模板不存在,`continue` 跳过 snapshot 重建(第 88-90 行)
4. **整个 config 块(含旧的 `template_snapshot`)被写回 DB**(第 148-150 行)
5. `BatchGenerate` 被调用时,若 `template_id` 仍为正整数但模板不存在,返回 `code: -2` 阻止保存
**关键缺陷**:若 config 块的 `template_id` 被前端置为 `0`(模板选单为空),则 `templateId > 0``false``BatchGenerate` 整个循环体被跳过,**无任何校验**直接写回。
### 根因 2P1幽灵 spec 持续污染 vr_goods_config
脏 config 块(含已删除模板的 `template_snapshot`)被写回 DB 后:
- 下次编辑商品时,`vr_goods_config` 仍含无效配置
- `GetGoodsViewData` 尝试加载模板(失败后将 `template_id` 置 null
- 但若 `save_thing_end` 在模板验证前先执行写回,无效配置再次被保存
- 循环往复,**幽灵 spec 永远无法被清理**
### 根因 3P2前端无 `vr_goods_config_base64` 输入保护
`AdminGoodsSaveHandle.php:29-35` 接收前端传入的 base64 编码配置:
- 无 schema 校验(不验证 `template_id` 是否为正整数)
- 无类型校验(不验证是否为数组)
- 管理员可直接 POST 恶意 JSON 注入 `vr_goods_config`
---
## 前端分析(参考 ticket_detail.html
### 硬删除场景下的 fallback
`SeatSkuService::GetGoodsViewData` 在模板不存在时:
- `vr_seat_template` 返回 `null`
- `goods_config.template_id``null`
- `goods_config.template_snapshot``null`
- `goods_spec_data` 返回空数组
前端 `ticket_detail.html` 读取 `seatMap = []``specBaseIdMap = []`,座位图不渲染。**设计正确**。
### 安全风险
1. **`loadSoldSeats()` 未实现**ticket_detail.html:375-383TODO 注释状态,无法标记已售座位。顾客可购买已售座位(需支付验证拦截)。
2. **`submit` 依赖 `specBaseIdMap`**(第 417 行):空时降级 `sessionSpecId`。理论上可操控座位数据选择任意座位。
3. **无直接 XSS**:后端输出均有编码,`seatMap` 和 `specBaseIdMap` 来自 DB 合规数据。
---
## 严重性分级
| 等级 | 数量 | 描述 |
|------|------|------|
| **P1** | 2 | 无效 template_id 静默保存;幽灵 spec 无法清理 |
| **P2** | 3 | Admin API 无 schema 校验;残留 snapshot 信息泄露specBaseIdMap 端侧无验证 |
| **低** | 0 | 无直接 XSS |
---
## 修复方案
### P1-1/P1-2拒绝无效 template_id必须
**AdminGoodsSaveHandle.php:158-173** 需在调用 `BatchGenerate` 前验证:
```php
foreach ($configs as $config) {
$templateId = intval($config['template_id'] ?? 0);
if ($templateId <= 0) {
return ['code' => -401, 'msg' => '票务配置中的 template_id 无效或已被删除'];
}
$exists = Db::name('vr_seat_templates')->where('id', $templateId)->find();
if (empty($exists)) {
return ['code' => -401, 'msg' => 'template_id [' . $templateId . '] 指向的场馆已不存在'];
}
$res = SeatSkuService::BatchGenerate(...);
if ($res['code'] !== 0) {
return $res;
}
}
```
### P2-1过滤无效 config 块(必须)
在写回 DB 之前过滤掉 `template_id <= 0` 的配置块:
```php
$validConfigs = array_filter($configs, function($c) {
return intval($c['template_id'] ?? 0) > 0;
});
if (empty($validConfigs)) {
return ['code' => -401, 'msg' => '票务商品必须包含至少一个有效场馆配置'];
}
Db::name('Goods')->where('id', $goodsId)->update([
'vr_goods_config' => json_encode($validConfigs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
```
---
## 结论
**幽灵 spec 的根因是后端未拒绝脏数据**,而非前端注入。`save_thing_end` 在模板验证失败时静默保留了无效的 config 块,导致 `vr_goods_config` 中的幽灵 spec 永远无法被清理。修复方向明确:任何 `template_id` 为空或指向不存在场馆的配置块,都必须被过滤或拒绝保存,并返回 `code: -401` 告知用户重新选择场馆。