# 路由文件安全审计报告（第一组）

审计日期：2026-08-04
审计范围：address.js, banner.js, cart.js, category.js, collector.js, coupon.js, download.js, product.js, stats.js
审计人：电商安全审计专家（子代理）
参考环境约定：DB 均走 utils/db 参数化 get/run/all；金额须整数分禁 float；统一 {code,message,data}；order.js 用 CAS 乐观锁防超卖、review.js 用幂等。
仅审计并写报告，未改动任何源代码。

---

## 文件:address.js
- ✅ 整体鉴权良好：第 7 行 `router.use(authUser)` 覆盖全部写操作，无缺鉴权。
- ✅ IDOR 防护到位：更新(L27)、设默认(L53)、删除(L66) 均带 `AND user_id=?` 归属校验，无法改删他人地址。
- ✅ SQL 全部参数化，无注入。
- ⚠️ 输入校验不足（低，L22-23）：`name`/`phone` 仅判非空，未校验长度与格式；`phone` 未做格式/位数限制，`zip_code`/`detail` 未限制长度，可能写入脏数据或超长文本。建议：
  ```js
  if (!name || !name.trim() || name.length > 50) return res.json(fail('姓名不合法'));
  if (!/^1[3-9]\d{9}$/.test(phone)) return res.json(fail('手机号格式不正确'));
  if ((zip_code||'') && !/^\d{6}$/.test(zip_code)) return res.json(fail('邮编格式不正确'));
  ```
- ℹ️ 第 43 行 `r.lastInsertRowid`：`utils/db.run` 为 better-sqlite3 `.run()`，确实返回 `lastInsertRowid`，用法正确，非 bug。
- ✅ 默认地址设置：更新/新增时先清 `is_default=0` 再置当前，逻辑正确。

## 文件:banner.js
- ✅ 纯只读公开接口，查询 `WHERE status=1` 过滤，参数化，无问题。
- ℹ️ 无鉴权非漏洞（banner 为公开展示数据，banner 写入在 admin-banner.js 管理端）。

## 文件:cart.js
- ✅ 第 7 行 `router.use(authUser)` 覆盖全部写操作，无缺鉴权。
- ✅ IDOR 防护到位：加/改/删/清空/选中 均带 `user_id=?` 归属条件。
- ✅ SQL 全部参数化（含 L29 动态列匹配用 `? IS NULL` 方式，安全性 OK）。
- ⚠️ 注意：新增(insert)与合并更新(L33/L36)是两步非原子，且未校验商品存在性与 `quantity` 合法区间：
  - ⚠️ 数量未设上限（高，L26,33,36,44）：`quantity` 由用户提交，`UPDATE quantity=quantity+?` 可累加任意大数字，且 PUT `/:id` 仅判 `quantity<1`，无上限校验。恶意用户可注入巨型数量，虽最终下单在 order.js 扣库存时有 CAS 校验，但购物车数据可被污染、接口日志/统计失真。建议：
    ```js
    const q = Math.floor(Number(quantity));
    if (!Number.isFinite(q) || q < 1 || q > 999) return res.json(fail('数量不合法'));
    ```
  - ⚠️ 未校验 `product_id` 存在（低，L27）：`SELECT *` 列表用 JOIN 拉产品信息，若 product_id 不存在则 entries 无产品字段但仍在购物车中；建议插入前查询商品存在及其上下架状态。
- ⚠️ `product_id`/`sku_id` 为字符串直接入库（低）：未做整数化，虽参数化无注入，但数据一致性差；建议 `+product_id` 强制数值。

## 文件:category.js
- ✅ 纯只读公开接口，参数化，`WHERE status=1` 过滤，无问题。

## 文件:collector.js
- ⚠️ **SSRF 高危（高）**：第 30-74 行 `/parse` 接受用户提交的 `url`（L32），在 L50 传入 `callCollectorAPI(apiUrl, appKey, url, platform)`（L92-113），其中 `url` 与配置的 `api_url` 一起被发送到外部服务。但更关键的是：配置的 `collector_api_url`（L44）来自 `system_config`（管理员配置，攻击面有限），而 `url` 用户的输入被 POST 到该第三方服务，若该服务为内部代理/可被重定向，存在 SSRF 利用链。此外 `/parse` 没有针对 `url` 做 scheme/内网 IP 校验。
  - 虽然本接口由 `router.use(authAdmin)`(L9) 限定管理员，但 SSRF 风险仍应收敛。建议对 `url` 做白名单平台域名校验并强制 https：
    ```js
    if (!/^https?:\/\/([a-z0-9-]+\.)*(taobao|tmall|1688|pinduoduo|yangkeduo|jd|suning)\.com/i.test(url)) {
      return res.json(fail('仅支持支持的电商平台链接'));
    }
    try { const u = new URL(url); if (u.protocol !== 'https:') { return res.json(fail('仅支持 https 链接')); } } catch(e){ return res.json(fail('链接格式不合法')); }
    ```
  - ⚠️ **Mock 兜底泄露属主数据源（低，L58-70）**：无采集配置时返回 `Math.random()` 生成的随机价格/库存（L61-62,69），会诱导前端拿到伪造价格，若后续采购流程信任该价格可能被滥用。建议 mock 分支仅用于显式测试开关，勿在生产返回随机真实域名图片与价格。
- ⚠️ `callCollectorAPI` 未设超时/重定向上限（中，L92-113）：`client.request` 无 timeout，若第三方服务不可达会长期挂起；且未限制重定向次数，配合用户可控 URL 可放大 SSRF。建议：
  ```js
  req.setTimeout(10000, () => { req.destroy(new Error('采集请求超时')); });
  ```
- ✅ `/config`、`/parse`、`/test` 均在 `router.use(authAdmin)`(L9) 或单独 `authAdmin`(L116) 保护下，鉴权完整。
- ✅ SQL 全部参数化。
- ⚠️ 配置中的 `app_key`/`secret`（L24-25）明文存 `system_config`（中）：secret 属敏感凭据，建议加密存储并避免在 `/test`(L120) 中明文回显 `app_key`。

## 文件:coupon.js
- ✅ `/user` 带 `authUser`(L8) 且强约束 `uc.user_id=?`，拼 `status` 用白名单 if/else（L12-14）而非拼接用户输入，无注入。状态筛选安全。
- ℹ️ `/list`（L21-25）公开返回全部可用优惠券（含 `value`/`min_amount` 等），为公开数据，非漏洞。若含内部敏感字段可考虑裁剪 —— 低。
- ℹ️ `status` 查询参数虽进入 SQL，但仅与白名单常量比较，安全。

## 文件:download.js
- ✅ 任意文件下载防护到位：`/download/:name` 用白名单 if/else（L24-25）映射固定路径，不接受用户输入拼接路径，无路径穿越。
- ✅ `res.download(filePath)` 使用服务端固定路径，非用户可控。
- ⚠️ 文件存在性未预校验（低，L22-32）：若 `dist/LiteMall-*.tar.gz` 不存在，`res.download` 会抛错导致异常；但不会构成安全漏洞。建议 `fs.existsSync` 预判并返回 `fail('包文件缺失')`。
- ℹ️ 下载接口为公共接口不鉴权（L21）：属业务设计（用于分发安装包），非授权绕过；但 `x-forwarded-for`(L28) 直接取头可被伪造用于灌日志。低。
- ✅ 统计接口 `/download/stats` 有 `authAdmin`(L12) 保护。

## 文件:product.js
- ✅ **ORDER BY 注入已防护**：L21-27 用白名单 `SORT_FIELDS`/`ASC_OPTS` 映射，`sort`/`order` 不直接进 SQL。良好。
- ✅ SQL 全部参数化；`list-by-ids`(L90-91) 用 `?` 占位符数组，无注入。
- ✅ 分页参数 `page`/`pageSize` 已做数值钳制（L9-10, 78-79, 100-101）。
- ℹ️ 公开只读接口（商品/详情/评价），无需登录，属业务设计，非漏洞。
- ⚠️ `keyword` 未用 `LIKE` 转义（低，L15）：`LIKE ?` 参数化防注入，但 `%`/`_` 通配符可被用户用作通配（语义问题非安全）。若需精确可转义：`keyword.replace(/[%_\\]/g, m => '\\' + m)`。
- ⚠️ 评价列表拉取 `pr.*`（L104）含可能冗余的内部字段（低）：如需最小化泄露建议显式列字段。
- ⚠️ 批量 `list-by-ids` 无 ids 数量上限（低，L87-93）：超大 `ids` 数组可致 SQL 语句过长/性能问题，建议 `ids.slice(0,200)`。

## 文件:stats.js
- ✅ 鉴权与门控良好：L9-14 功能门控 + L16 `router.use(authAdmin)`，admin 批量操作（L181/195/205/215）另有 `authAdmin`，防护完整。
- ⚠️ **SQL 注入（中）**：L26-32 `/sales-trend` 的 `interval` 由 `range` 直接拼接进 SQL：
  ```js
  const { range = 7, unit = 'day' } = req.query;          // L20 用户可控字符串
  let interval = `-${range} day`;                          // L22
  ...
  AND created_at >= date('now','localtime','${interval}')  // L31 直接插值
  ```
  `range`/`unit` 未做数值/枚举校验即拼串。虽然作者本意是数值，但 `DATETIME(date('now','localtime','${interval}'))` 中 `range` 若为 `1) OR 1=1--` 之类可被 SQLite 解析函数当作 modifier 注入（SQLite 的 date modifier 支持如 `'+1 month'`、函数表达式）。当前 DB 用参数化 get/all，此处却绕过参数化直接插值，违反工程约定。**必须改为数值强转 + 白名单**：
  ```js
  const range = Math.max(1, Math.min(365, Math.floor(Number(req.query.range) || 7)));
  const unit = ['day','week','month'].includes(req.query.unit) ? req.query.unit : 'day';
  // 并用参数绑定：date('now','localtime',?)，参数为 `-${range} day`
  ```
- ⚠️ SQL 注入（中）：L49-56 `/user-growth` 的 `days` 虽做了 `+days`（L53 `-${+days} day`）数值化，但 `+days` 对非数字返回 `NaN`，拼接成 `-NaN day` 不报错但语义异常；且仍是字符串插值，建议统一样式改为参数化。低-中：至少先 `Math.floor(Number(days))` 并钳制范围。
- ⚠️ CSV 注入（中，L96-156）导出三处（orders/users/products）将数据库内容直接包进 `"..."` 拼 CSV（L112,134,150）。若订单备注/用户昵称含 `=`, `+`, `-`, `@` 开头或含逗号/换行，导出到 Excel 可能触发 **CSV 公式注入/列感染**。数据库内容由管理员自己写入影响有限，但其值亦可能来自用户提交（如订单 remark、收货人）。建议清洗：对以 `=+-@` 开头的字段前置单引号并转义双引号：
  ```js
  const esc = (v) => { v = String(v ?? ''); v = v.replace(/"/g,'""'); if (/^[=+\-@\t\r]/.test(v)) v = "'" + v; return v; };
  ```
- ⚠️ 分页/排序注入：L160-171 `/operation-logs`、L195-202 均 `module`/`action` 参数化，安全；无排序参数拼接。✅
- ⚠️ 批量发货缺 CAS（中，L215-224）：`/batch/ship` 更新带 `AND status='pending_ship'` 条件，具备状态机防护（不会把非待发货订单发掉），好；但未检查 `run` 影响行数，且无事务包裹（L218-222 for 循环逐条，若中途失败已提交部分）。建议用 `transaction()` 包裹保证原子性。
- ⚠️ 批量上下架 L210 用占位符数组安全；L208 `get('?').constructor` 为无意义占位，不影响安全（死代码）。低。
- ⚠️ `/recent-activity`(L175) 暴露最近订单号/金额与用户手机号到登录页（低）：`SELECT order_no,pay_amount` 与 `SELECT id,nickname,phone` 直接公开，建议脱敏手机号。
- ⚠️ `/export/users`(L128-130) 导出全部用户**手机号/余额**（高敏）：虽有 `authAdmin` 保护，但应确认读 CSV 范围；且 CSV 内容直接泄露，建议敏感导出（含余额/手机号）增加二次确认/审计日志。属配置类提醒，非当前代码 bug。

---

## 汇总（按严重度）

| 严重度 | 文件:行 | 问题 |
|--------|---------|------|
| 高 | collector.js:32,50,92-113 | SSRF（用户 URL 进外部请求，未校验 scheme/内网/超时） |
| 中 | stats.js:20-32 | SQL 注入式插值：`interval` 由 `range` 拼接，绕过参数化 |
| 中 | stats.js:96-156 | CSV 导出公式注入（备注/昵称/地址可污染表格） |
| 中 | stats.js:53 | `days` 字符串插值（虽 `+days` 数值化，应改参数化+钳制） |
| 中 | cart.js:26,33,36,44 | 购物车数量无上限校验（可注入超大数量） |
| 中 | stats.js:215-222 | 批量发货非事务、未校验影响行数（CAS 缺失） |
| 中 | collector.js:24-25,120 | 采集密钥明文存储并回显 |
| 低 | address.js:22 | 姓名/手机号/邮编格式长度未校验 |
| 低 | cart.js:27 | product_id/sku_id 未校验存在性与数值化 |
| 低 | product.js:15 | keyword LIKE 通配符未转义（语义问题） |
| 低 | product.js:87-93 | list-by-ids 无 ids 数量上限 |
| 低 | download.js:22-32 | 文件存在性未预判；x-forwarded-for 可伪造灌日志 |
| 低 | stats.js:175 | 首页暴露订单号/用户手机号 |
| 低 | collector.js:58-70 | mock 兜底返回随机价格/库存 |

### 无问题的文件
- banner.js：✅ 纯只读公开，参数化。
- category.js：✅ 纯只读公开，参数化。
- coupon.js：✅ 鉴权+白名单筛选正确，无注入。
- product.js：✅ 排序白名单防 ORDER BY 注入，参数化完整（仅轻微低危项）。
- address.js：✅ 鉴权与 IDOR 防护到位（仅输入校验低危项）。

### 关键共性风险提示
1. **`stats.js` 是唯一绕过参数化做字符串 SQL 插值的文件**（L31, L53），违反「DB 全部走参数化」约定，需优先修复。
2. CSV 导出（stats.js）含公式注入面，建议统一清洗函数。
3. 购物车数量上限缺失与批量发货非事务，均属「逻辑漏洞」范畴，建议对齐 order.js 的 CAS/幂等写法。
