02-核心代码审8条.md

🕐 时效标注(2026-09-13 文档治理):近期设计件,口径以 TASKS 现行与契约 v1.3 为准。

审查结论

共发现 8 条问题,按严重度排序。未发现明确的服务端越权漏洞;但图片外链未限制、统一请求错误契约不完整,以及 safe-img 的异步竞态,可能造成安全风险或用户侧错误展示。

行号按所给代码片段估算。

1. request.ts:HTTP 200 但业务失败时被当作成功返回

文件+行号: request.ts:31-42、70-73

严重度:P1

类型:正确性 / 统一请求设计偏差

问题

unwrap() 只要发现对象同时包含 code 或 ok,以及 data,就直接返回 data,没有判断业务成功状态。

例如后端返回:

{
  "code": 403,
  "data": null,
  "detail": "无权访问"
}

或者:

{
  "ok": false,
  "data": null,
  "message": "操作失败"
}

当前实现会直接解析为 null,业务层无法区分“接口成功但数据为空”和“接口业务失败”。

影响

建议

明确成功契约,例如:

if ('ok' in boxed) {
  if (boxed.ok !== true) throw new Error(...)
  return boxed.data as T
}

if ('code' in boxed) {
  if (boxed.code !== 0 && boxed.code !== 200) throw new Error(...)
  return boxed.data as T
}

具体成功码规则材料未涉及,应以《后端API契约规范》为准。


2. safe-img.ts:src 变更后,旧图片请求的异步 onError 可能污染新候选链

文件+行号: safe-img.ts:48-53、57-66

严重度:P1

类型:异步竞态 / 候选链正确性

问题

组件在 src 变化时会重置:

this.setData({
  cands,
  idx: 0,
  cur: cands[0],
})

但旧候选图片的 error 事件可能在新 src 已经设置后才到达。onError() 没有校验该事件是否属于当前候选链,直接读取当前 this.data.cands 和 idx 并递增。

典型场景

  1. 旧图片 A 正在加载;
  2. 列表复用,src 改为图片 B,候选链重置;
  3. A 加载失败,触发旧 onError;
  4. 组件误将 B 的候选下标从 0 推进到 1,导致跳过 B 的主候选。

影响

建议

引入链路版本号或候选 URL 校验:

data: {
  generation: 0,
  ...
}

resetChain(src: string) {
  const generation = this.data.generation + 1
  ...
  this.setData({ generation, ... })
}

onError(e) {
  const failedSrc = e.detail?.errMsg // 需根据小程序事件结构确认
  if (failedSrc !== this.data.cur) return
  ...
}

也可以为每次重置生成 token,并在事件回调中校验 token。


3. img-resolve.ts:外链图片完全直通,缺少协议、域名和来源限制

文件+行号: img-resolve.ts:36-37、62-63

严重度:P1

类型:安全 / 设计边界

问题

以下逻辑会将任意 http:// 或 https:// 地址直接作为图片地址:

if (p.indexOf('http://') === 0 || p.indexOf('https://') === 0) return [p]

resolveImg() 也同样直接返回任意外链:

if (p.indexOf('http://') === 0 || p.indexOf('https://') === 0) return p

没有限制:

影响

客户端图片加载本身不等同于服务端 SSRF;材料中没有服务端代理逻辑,因此不能认定存在 SSRF。

建议


4. img-resolve.ts:imgCandidates() 与注释描述的空输入行为不一致

文件+行号: img-resolve.ts:32-35、41-43

严重度:P2

类型:正确性 / 设计意图偏差

问题

代码和注释存在不一致:

if (!isStr(raw)) return [FALLBACK_PLACEHOLDER]
const p = raw.trim()
if (!p) return [FALLBACK_LOGO]

文件头注释称:

空键/无效输入 → placeholder.png

但空字符串实际返回的是:

FALLBACK_LOGO = '/images/fallback/logo.png'

同时 safe-img.ts 的注释又称空输入会直接使用:

FALLBACK_IMG = '/images/draft/logo.png'

因此同一种空输入,在不同路径下可能出现三种不同的占位资源:

影响

建议

统一占位策略,建议:

if (!isStr(raw) || !raw.trim()) {
  return [FALLBACK_PLACEHOLDER]
}

safe-img.ts 的最终 fallback 也应复用同一常量或由统一配置注入。当前材料未说明三张图片的实际存在性,无法判断是否会 404。


5. img-resolve.ts:非迁移模式下的 draft 路径拼接未经过统一规范化

文件+行号: img-resolve.ts:44-45、resolveImg():56-59

严重度:P2

类型:正确性 / 设计偏差

问题

迁移开关关闭时:

return ['/images/draft/' + name, FALLBACK_PLACEHOLDER]

以及:

return '/images/draft/' + p.replace('/images/content/', '')

没有复用 cosKey() 中的分段清理和编码逻辑。

虽然前置判断限制了路径以 /images/content/ 或 /images/draft/ 开头,但 name 仍可能包含:

这会导致本地路径与迁移开启时的 COS 路径解析规则不一致。

影响

建议

先提取并校验逻辑资源名,再生成本地路径:

const segs = name.split('/').filter(Boolean)
if (segs.some(s => s === '.' || s === '..')) {
  return [FALLBACK_PLACEHOLDER]
}
const safeName = segs.map(encodeURIComponent).join('/')

是否允许嵌套目录,材料未明确,应以图片键规范为准。


6. request.ts:mock 生产者同步抛错时不会转换为 rejected Promise

文件+行号: request.ts:47-49

严重度:P2

类型:错误处理

问题

当前代码:

return Promise.resolve(options.mock())

options.mock() 会先同步执行。如果 mock 函数抛出异常,该异常会直接从 request() 同步抛出,而不是返回 rejected Promise。

调用方若统一使用:

request(...).catch(...)

可能无法捕获该错误。

建议

改为:

return Promise.resolve().then(() => options.mock!())

或:

try {
  return Promise.resolve(options.mock())
} catch (err) {
  return Promise.reject(err)
}

这样 mock 与真实请求具有一致的异步错误语义。


7. request.ts:自动重试没有取消机制,页面销毁后仍可能发起请求

文件+行号: request.ts:76-83

严重度:P2

类型:异步 / 资源管理

问题

GET 请求失败后固定延迟 1200ms 重试:

setTimeout(() => doRequest(false), 1200)

没有保存 timer,也没有提供取消能力。即使页面已经卸载、用户离开页面或业务请求已经不再需要,重试仍会继续执行。

影响

建议

具体小程序版本是否支持对应 abort 能力,材料未涉及。


8. request.ts:后端 detail 原文直接进入前端 Error,可能泄露内部错误信息

文件+行号: request.ts:65-68、81-83

严重度:P2

类型:信息泄露 / 错误处理

问题

代码直接将后端返回的 detail 暴露给业务层:

const detail = (res.data as { detail?: string })?.detail || '登录已过期'
reject(new Error(detail))

以及:

const detail = typeof raw === 'string' && raw
  ? raw
  : `请求失败(${res.statusCode})`

如果后端 detail 包含 SQL 错误、内部路径、堆栈、用户标识或调试信息,前端页面可能直接展示给用户或上报日志。

影响

建议

统一映射错误码到用户可见文案,原始 detail 仅在受控调试模式记录;生产环境不要直接展示后端原文。

材料未提供后端 detail 的内容规范,因此无法进一步确认是否已经存在敏感字段泄露。