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

10 KiB
Raw Blame History

安全审计报告:幽灵 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_idselected_roomsselected_sectionssessionstemplate_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 行):

    $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 行):
    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 行):

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 行)

关键代码:

// 检查是否有关联商品(使用 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 注入风险:$idintval,安全
  • 审计日志已记录(第 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_configtemplate_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已售座位标记以提升用户体验和防止超卖。