惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

雷峰网
雷峰网
MongoDB | Blog
MongoDB | Blog
D
Docker
Martin Fowler
Martin Fowler
人人都是产品经理
人人都是产品经理
GbyAI
GbyAI
Jina AI
Jina AI
酷 壳 – CoolShell
酷 壳 – CoolShell
M
MIT News - Artificial intelligence
腾讯CDC
阮一峰的网络日志
阮一峰的网络日志
H
Hackread – Cybersecurity News, Data Breaches, AI and More
N
Netflix TechBlog - Medium
B
Blog RSS Feed
云风的 BLOG
云风的 BLOG
Blog — PlanetScale
Blog — PlanetScale
Vercel News
Vercel News
The Cloudflare Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
有赞技术团队
有赞技术团队
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
I
InfoQ
U
Unit 42

FreeBuf网络安全行业门户

Anthropic 强化 Claude 安全防护:AI 模型在评估中未经授权访问真实系统 告警堆到 100 万条那天,我决定自己写一个“会用 AI“的安全运营中心 - FreeBuf网络安全行业门户 新手勇闯网络安全 | 网络通信基础(三) - FreeBuf网络安全行业门户 FreeBuf早报 | 宇树G1 EDU人形机器人漏洞可致root级远程代码执行;Claude平台遭攻击 - FreeBuf网络安全行业门户 保障 Claude Code 安全:全新 Compliance API、本地可见性与身份治理 Apache Shiro rememberMe 反序列化漏洞:从 Cookie 到 RCE 免费路由器 DNS 调整可拦截家庭网络中的恶意软件和钓鱼攻击 - FreeBuf网络安全行业门户 黑客利用信息窃取恶意软件窃取 Claude 登录会话,劫持账户 - FreeBuf网络安全行业门户 奇安信2026半年报:营收跌了14%,亏损却砍半,这笔账怎么算? - FreeBuf网络安全行业门户 出厂即后门:一台深圳路由器里藏着的两个钉子户——SPEAKINGSTONE 与 DARKLANTERN 拆解 - FreeBuf网络安全行业门户 一个 STOR 文件名,让 PostgreSQL 替你执行命令——ProFTPD mod_sql 认证后 RCE 拆解(CVE-2026-42167) 让 root 替你写文件:cPanel 域停放附加域提权拆解(CVE-2026-65643) - FreeBuf网络安全行业门户 量化公司策略代码全生命周期安全平台建设方案 - FreeBuf网络安全行业门户 AI Agent 工具调用的信任边界:从 MCP 攻击面到"Agent 信任链"模型 新手勇闯网络安全 | 网络通信基础(二) - FreeBuf网络安全行业门户 HW 蓝队·异常外联流量溯源实战:那些告警背后真正在发生什么 - FreeBuf网络安全行业门户 Apache Tomcat CVE-2026-65182 分析:一次 SecurityConstraint 最长匹配逻辑缺陷导致的访问控制绕过 安全度量:拿什么向管理层证明安全的价值 - FreeBuf网络安全行业门户 曼彻斯特机场集团确认客户数据遭窃取 - FreeBuf网络安全行业门户 黑客利用 Claude 和 ChatGPT 入侵多个政府机构 报告:思科或以超2.5亿美元收购AI Agent安全初创公司Astrix Security 【安全圈】国家网络安全通报中心:近期集中爆发多起供应链投毒攻击事件 员工自费买Token引爆内网?揭秘AI中转站的四种“投毒”手段与供应链危机 AI 路由漏洞可被利用注入恶意代码并窃取敏感数据 黑客滥用 GitHub 和 GitLab 托管恶意软件并实施凭证钓鱼攻击 Claude 在数分钟内发现存在 13 年的 ActiveMQ 远程代码执行漏洞 2026攻防演练红队高频面试题30个(含答案)! AI Agent沙箱之ANOLISA内置沙箱 XXE 漏洞原理剖析:DTD、实体解析机制与攻击链构建 谷歌预警引发连锁反应:Cloudflare正积极调整抗量子加密战略优先级
黑名单只防了 AWS?Directus 默认配置下 SSRF 直通国产云 met...
关 注 0 文章数 0 关注者 · 2026-08-31 · via FreeBuf网络安全行业门户

环境:Ubuntu 22.04 / Docker / lobehub/lobehub:v2.2.13(2026-08-01 发布)
目标版本:v2.2.9(修复前)vs v2.2.13(最新稳定)vs v2.2.14-canary.74(8 月 12 日最新)

写在前面

2026 年 7 月 2 日,LobeChat 一口气公开了四个漏洞:CVE-2026-59095(SSRF,7.7 分)、CVE-2026-58580(BOLA,5.9 分)、CVE-2026-58578(ReDoS,6.5 分)、CVE-2026-59098(RAG 访问控制,6.5 分)。官方在同一天把四个修复 commit 合进了 canary 分支。

我把四个修复的 diff 全拉下来逐行看了一遍,注意到一个细节:SSRF 那个修复只覆盖了两个端点——agentSkills.importFromUrl 和生成图片时的 fetchImageFromUrl。但 LobeChat 的 bot 平台功能(Discord/Slack/微信/飞书机器人)里,有一堆地方会拿用户给的 URL 去发请求,官方一个都没碰。

这篇文章讲我怎么顺着这个线索找到第五个洞:botMessage 接口的附件下载路径,从 v2.2.9 到 8 月 12 日发布的 v2.2.14-canary.74,一直都是裸 fetch,没有任何 SSRF 防护。我在最新版上部署验证,伪造凭据创建 bot,成功让服务器主动请求了宿主机内网地址和腾讯云 metadata 服务。

文章的结构是:先讲四个已公开漏洞和官方的修法(另外三个修复我也逐个验证了是否完整),再讲 ssrfSafeFetch 防护机制本身,然后引出 botMessage 附件下载这条没人管的路径,接着是 OSS 版权限为什么拦不住普通用户,然后完整实战验证,最后总结成"修复完整性审计"这个方法论,附一个自动化审计工具。

核心发现:官方修了 6 处同类型端点,漏了 4 处,漏掉的 4 处还是同一个入口(botMessage),最新版依然能打。

一、四个洞和官方的修法

1.1 SSRF:CVE-2026-59095(7.7 分)

漏洞本身不复杂:两个 tRPC 端点会让服务器去 fetch 用户提供的 URL。NVD 对它的描述是 "allows authenticated attackers to direct internal HTTP requests to arbitrary URLs by supplying user-controlled... "——认证用户就能让服务器发内网请求。这个洞最初是以私有 advisory 提交的(GHSA-53h9-fmjf-frwr),7 月 2 日随修复一起公开。

第一个是 agentSkills.importFromUrl,用来从任意 URL 导入技能包。修复前的代码长这样:

// 修复前:直接 fetch 用户给的 URL
response = await fetch(input.url, { signal: controller.signal });

第二个是生成图片时对图片来源 URL 的抓取(fetchImageFromUrl),同一个问题。

官方的修复是给这两个端点套上项目里现成的 ssrfSafeFetch 封装:

// apps/server/src/services/skill/importer.ts(修复后)
response = await ssrfSafeFetch(input.url, { signal: controller.signal });
// apps/server/src/services/generation/index.ts(修复后)
const response = await ssrfSafeFetch(url, { headers: fetchHeaders });

PR 编号 #16601,7 月 2 日合并。我特意看了这个 PR 的 diff:改了 5 个文件,涉及 skill 导入、图片生成、web-crawler 的 naive 实现,改动量不大。就这些。

1.2 BOLA:CVE-2026-58580(5.9 分)

这是 MessageModel 的问题:5 个写方法——updateMessagePlugin、updatePluginState、updatePluginError、updateTTS、updateTranslate——在数据库查询时只按行 id 过滤,没带 userId 谓词。

后果:在多用户的 server-database 部署里,登录用户拿别人的消息 id 就能改对方的插件状态、TTS 配置、翻译内容。比如 updateTranslate,v2.2.9 里是这样:

// v2.2.9:query 带 translatesOwnership,但 INSERT 路径没有
updateTranslate = async (id: string, translate: Partial<ChatTranslate>) => {
  const result = await this.db.query.messageTranslates.findFirst({
    where: and(eq(messageTranslates.id, id), this.translatesOwnership()),
  });

  // 如果不存在,直接插入——这里没有归属校验
  if (!result) {
    return this.db.insert(messageTranslates).values({
      ...translate,
      id,
      userId: this.userId,
      workspaceId: this.workspaceId ?? null,
    });
  }
  ...
}

updateTranslate 的 INSERT 路径有个问题:id 是调用方传的,userId 却是当前用户。如果攻击者知道别人的 message id,往 messageTranslates 表插一行,把别人的消息 id 关联上自己的翻译内容——别人看到自己的消息翻译被篡改了。而 UPDATE 路径带 translatesOwnership() 只挡了已存在的行,挡不住"插入伪造关联"。

官方修复是在 router 层统一补了资源归属校验:

// canary 修复后:先校验消息归属
updateTTS: messageProcedure
  .use(withScopedPermission('message:update'))
  .mutation(async ({ input, ctx }) => {
    await assertCanUseMessageTargets(guardCtx(ctx), [input.id]);
    ...
  }),

我验证了这个修复的完整性:把 message router 里全部 23 个写方法过了一遍,每个都有对应的资源断言,没有漏网的。这个洞修得干净,只能当对照组。

1.3 ReDoS:CVE-2026-58578(6.5 分)

GitHub 技能导入功能有个正则问题。https://github.com/owner/repo/tree/branch/<path>里的 path 段被直接拼进正则:

// 修复前:用户输入直接进正则
const basePathPattern = new RegExp(
  `^[^/]+/${basePath.replaceAll(/^\/|\/$/g, '')}/SKILL\\.md$`,
);

basePath 来自https://github.com/owner/repo/tree/branch/<path>的 path 段,完全用户可控。构造(a+)+之类的模式就能让正则灾难性回溯,卡死 Node.js 事件循环;构造[invalid直接抛 SyntaxError。

basePath 拼接前经过了 replaceAll(/^/|/$/g, ''),只去掉了首尾斜杠。也就是说,攻击者传https://github.com/o/r/tree/b/(a+)+$这种,basePath 变成(a+)+$,正则就变成^[^/]+/(a+)+$/SKILL\.md$——(a+)+配合超长的输入字符串(GitHub zip 包里的路径)就会指数级回溯。

官方修法是把正则改成纯字符串匹配:

// 修复后:不用正则了
const normalizedBasePath = basePath.replaceAll(/^\/|\/$/g, '');
const suffix = `/${normalizedBasePath}/SKILL.md`;
const matchWithBasePath = allPaths.find((path) => path.endsWith(suffix));

这个修复我也确认过,是完整的。从修法看,就是把正则匹配换成了字符串匹配,能用字符串解决的就别用正则。

1.4 RAG 访问控制:CVE-2026-59098(6.5 分)

KnowledgeBaseSearchService 做语义搜索时,knowledgeBaseFiles 的查询只按 knowledgeBaseId 过滤,没带 workspace 谓词。攻击者拿别人的 knowledgeBaseId 就能解析出对方的 fileIds,再结合语义搜索接口把文件内容搜出来。

// 修复前:没带 workspace 过滤
const knowledgeFiles = await this.serverDB.query.knowledgeBaseFiles.findMany({
  where: inArray(knowledgeBaseFiles.knowledgeBaseId, knowledgeIds),
});

// 修复后:加了 buildWorkspaceWhere
const knowledgeFiles = await this.serverDB.query.knowledgeBaseFiles.findMany({
  where: and(
    inArray(knowledgeBaseFiles.knowledgeBaseId, knowledgeIds),
    buildWorkspaceWhere(
      { userId: this.userId, workspaceId: this.workspaceId },
      knowledgeBaseFiles,
    ),
  ),
});

两条分支(vector 搜索和 BM25 搜索)我都跑了一遍源码,都带 workspace 过滤。这条也修干净了。

1.5 四个修复看完,一个共性

四个洞的修法各有各的套路:SSRF 是给调用点套防护,BOLA 是 router 层补资源断言,ReDoS 是正则改字符串匹配,RAG 是加 workspace 谓词。但有个共性:除了 SSRF,另外三个修复都覆盖了所有同类位置——BOLA 的 23 个写方法、ReDoS 的 findSkillMd、RAG 的两条搜索分支,我都逐个确认过,没有漏网的。

拿 BOLA 修复举例:assertCanUseMessageTargets 只在 workspace 模式下生效,个人模式(没有 workspaceId)直接 return。个人模式靠 model 层的 buildWorkspaceWhere 兜底——个人数据本来就是 userId + workspace_id IS NULL 过滤的,所以这条路是安全的。这个分层我没觉得有问题。

只有 SSRF 那个修复,覆盖范围明显偏小:两个端点,改完就完。当时我就想,是不是还有其他 fetch 用户 URL 的地方没被看到。这个念头直接引出后面的第五个洞。

二、官方防护 ssrfSafeFetch 长什么样

在讲第五个洞之前,得先搞清楚官方的防护机制本身。ssrfSafeFetch 在 packages/ssrf-safe-fetch/index.ts,核心实现不长:

export const ssrfSafeFetch = async (
  url: string,
  options?: RequestInit,
  ssrfOptions?: SSRFOptions,
): Promise<Response> => {
  // 环境变量开关:SSRF_ALLOW_PRIVATE_IP_ADDRESS=1 会放行私网
  const envAllowPrivate = process.env.SSRF_ALLOW_PRIVATE_IP_ADDRESS === '1';
  const allowPrivate = ssrfOptions?.allowPrivateIPAddress ?? envAllowPrivate;

  const agentOptions: RequestFilteringAgentOptions = {
    allowIPAddressList: ssrfOptions?.allowIPAddressList ??
      process.env.SSRF_ALLOW_IP_ADDRESS_LIST?.split(',').filter(Boolean) ?? [],
    allowMetaIPAddress: allowPrivate,
    allowPrivateIPAddress: allowPrivate,
    denyIPAddressList: [],
  };

  // 用 request-filtering-agent 的 agent 拦截私网连接
  const httpAgent = new RequestFilteringHttpAgent(agentOptions);
  const httpsAgent = new RequestFilteringHttpsAgent(agentOptions);

  const response = await fetch(url, {
    ...options,
    agent: (parsedURL: URL) => (parsedURL.protocol === 'https:' ? httpsAgent : httpAgent),
  } as any);
  ...
};

原理是 request-filtering-agent:在 TCP 连接建立前解析 DNS,如果目标 IP 落在私网段(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8、169.254.0.0/16 这些)就拒绝连接。而且 agent 是每个重定向跳都会调用的,所以 302 跳到内网也会被拦。

防护本身没毛病,问题在于:它只被用在官方修的那两个端点上。项目里 fetch 用户可控 URL 的地方远不止两个,而"没套 ssrfSafeFetch 的地方"就是裸奔。

这个封装的测试覆盖其实挺全的——私网 IP、云 metadata、file:// 协议、环境变量开关、响应体大小限制都有单测。防护能力是到位的,缺的是调用点覆盖。

另外,这个封装还支持通过环境变量放行特定 IP:

# 允许特定内网 IP(保持其他私网拦截)
SSRF_ALLOW_IP_ADDRESS_LIST=192.168.1.100,10.0.0.50
# 完全放行私网(不推荐,仅调试用)
SSRF_ALLOW_PRIVATE_IP_ADDRESS=1

这些都是合理的设计。但对 botMessage 的裸 fetch 来说,这些开关一个都管不到——它压根不走 ssrfSafeFetch。

三、botMessage 附件下载:一条没人管的路径

3.1 从入口到出口的完整链路

我在全仓库搜裸 fetch 调用,结果 bot 平台四个 sendAttachments.ts 全是裸 fetch:

grep -n "fetch(attachment.fetchUrl" apps/server/src/services/bot/platforms/discord/sendAttachments.ts
# 33: const response = await fetch(attachment.fetchUrl, {


四个平台(Discord/Slack/微信/飞书)的 sendAttachments.ts 代码几乎一样:

// apps/server/src/services/bot/platforms/discord/sendAttachments.ts
const loadAttachmentBuffer = async (
  attachment: BotMessageAttachment,
): Promise<Buffer | undefined> => {
  if (attachment.data) {
    // 优先 base64
    return Buffer.from(attachment.data, 'base64');
  }
  if (attachment.fetchUrl) {
    try {
      // 这里!用户给的 URL 直接 fetch
      const response = await fetch(attachment.fetchUrl, {
        signal: AbortSignal.timeout(15_000),
      });
      if (response.ok) {
        return Buffer.from(await response.arrayBuffer());
      }
    } catch (error) {
      log('loadAttachmentBuffer: fetch failed for %s: %O', attachment.fetchUrl, error);
    }
  }
  return undefined;
};

attachment.fetchUrl 从哪来?往上追,是 tRPC 接口 botMessage.sendMessage / sendDirectMessage 的输入:

// apps/server/src/routers/lambda/botMessage.ts
const attachmentsInputSchema = z.array(
  z.object({
    data: z.string().optional(),
    fetchUrl: z.string().url().optional(),
    mimeType: z.st