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

134 lines
5.3 KiB
Markdown
Raw Permalink 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.

# 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` 告知用户重新选择场馆。