5.3 KiB
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)
根因 1(P1):无效 template_id 配置块未被过滤
文件:AdminGoodsSaveHandle.php:148-173
当 vr_goods_config 中存在 template_id 指向已删除场馆的配置块时:
save_thing_end从 DB 加载 config(第 61-66 行)- 遍历 configs 尝试重建
template_snapshot(第 77 行) - 若模板不存在,
continue跳过 snapshot 重建(第 88-90 行) - 整个 config 块(含旧的
template_snapshot)被写回 DB(第 148-150 行) BatchGenerate被调用时,若template_id仍为正整数但模板不存在,返回code: -2阻止保存
关键缺陷:若 config 块的 template_id 被前端置为 0(模板选单为空),则 templateId > 0 为 false,BatchGenerate 整个循环体被跳过,无任何校验直接写回。
根因 2(P1):幽灵 spec 持续污染 vr_goods_config
脏 config 块(含已删除模板的 template_snapshot)被写回 DB 后:
- 下次编辑商品时,
vr_goods_config仍含无效配置 GetGoodsViewData尝试加载模板(失败后将template_id置 null)- 但若
save_thing_end在模板验证前先执行写回,无效配置再次被保存 - 循环往复,幽灵 spec 永远无法被清理
根因 3(P2):前端无 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返回nullgoods_config.template_id置nullgoods_config.template_snapshot置nullgoods_spec_data返回空数组
前端 ticket_detail.html 读取 seatMap = [] 和 specBaseIdMap = [],座位图不渲染。设计正确。
安全风险
loadSoldSeats()未实现(ticket_detail.html:375-383):TODO 注释状态,无法标记已售座位。顾客可购买已售座位(需支付验证拦截)。submit依赖specBaseIdMap(第 417 行):空时降级sessionSpecId。理论上可操控座位数据选择任意座位。- 无直接 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 前验证:
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 的配置块:
$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 告知用户重新选择场馆。