Skip to content

fix(http): JSON 错误响应绕过 maxBytes,OAuth 请求可整段缓冲超大响应体 #10

Description

@LIghtJUNction

问题与核验版本

LmmHttp.request() 只在成功响应分支检查 maxBytes;非 2xx 且 Content-Type 为 application/json 时,为读取错误码直接调用 await response.text(),没有大小限制。

核验:main@bd51fe1f001b2528b04d80fb7d09e9e243aacadd。

这里是 OAuth/目录/余额 HTTP 客户端的资源边界问题,不是 #1 的模型 SSE 错误分类,也不是 #3 的模型请求重试。已查看现有 issues 和开放 PR,未发现同一缺陷;核查时没有开放 PR。

已执行的离线复现

在 Node.js v22.16.0 下直接执行原始 src/http.ts 与 src/protocol.ts,仅用 --experimental-strip-types 去掉类型;没有改写方法或替换校验函数。注入原生 Response 和计数的 ReadableStream 作为 fetch 返回值,完全没有建立网络连接。

本地源文件已按 Git blob 算法核对:

http.ts     ef3d18b61f6f69bbaaddecf634bb79c3d9e8a9f3
protocol.ts d0d422622eaeae3539d5aee0fadece7f64c6830a

四组响应均为有效 JSON、同一个 4 MiB 内容,按 16 KiB 分块提供,stream highWaterMark=0;调用 form(),上限应为 64,000 字节。

HTTP 状态 实际读取字节 主动取消剩余 body 返回类别
200 65,536 是 invalid_response
401 4,194,304 否,已读到 EOF unauthorized
429 4,194,304 否,已读到 EOF rate_limited
500 4,194,304 否,已读到 EOF upstream_unavailable

成功分支在跨过阈值的那一个 chunk 后停止是正常的;错误分支则消费全部 4 MiB。4 MiB 仅是本次有意控制的小样本,不是代码的实际上限,也没有进行 OOM/线上压测。

可将以下脚本放在仓库根目录,以 node --experimental-strip-types probe-http.mjs 复现:

import { LmmHttp } from './src/http.ts';
const payload = new TextEncoder().encode(JSON.stringify({
  padding: 'x'.repeat(4 * 1024 * 1024 - 14),
}));
for (const status of [200, 401, 429, 500]) {
  let offset = 0, cancelled = false;
  const http = new LmmHttp({fetch: async () => new Response(
    new ReadableStream({
      pull(controller) {
        if (offset === payload.length) { controller.close(); return; }
        const end = Math.min(offset + 16384, payload.length);
        controller.enqueue(payload.subarray(offset, end));
        offset = end;
      },
      cancel() { cancelled = true; },
    }, {highWaterMark: 0}),
    {status, headers: {'content-type': 'application/json'}},
  )});
  let code;
  try { await http.form('/api/oauth2/token', {grant_type: 'fixture'}); }
  catch (error) { code = error.code; }
  console.log({status, maxBytes: 64000, readBytes: offset, cancelled, code});
}

影响

登录、刷新授权等失败处理,反而绕过成功路径已有的内存保护。服务端或反向代理返回体积较大的 JSON 错误时,客户端会先完整读取并解析它,才报告 HTTP 错误。已有超时限制不能代替字节上限。

这里只证明客户端资源边界缺失;未证明普通用户能够控制正式 LMM 服务的错误体,未观察线上内存故障或泄露凭据。

修复建议与验收

  • 抽出一个有上限且会取消 reader 的 JSON/body 读取函数,成功与错误路径复用;错误诊断可使用更小的独立预算,但不能是无界 text()。
  • 超限立即取消剩余响应;不要依赖 Content-Length,必须覆盖分块响应和缺失/不可信长度。
  • 保留有效且小体积的 IP_ACCESS_ROUTE_REJECTED 专用类别,以及 401/403、429、5xx 的安全状态分类;不把错误正文或凭据输出给用户。
  • 内层解析 catch 不应吞掉请求取消/超时后继续将其伪装为授权失效;补取消期间读取错误体的测试。
  • 添加刚好达到上限、超过上限、畸形 JSON、小体积 IP 策略错误、401/429/500、读取中取消等回归;断言读取量和 body 释放,而不只断言最终异常文字。

验证边界:已运行上述原方法离线测试;未运行完整插件测试套件、真实 OAuth 登录或生产环境测试。此次仅提交问题,不修改代码、不部署。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions