Skip to content

Conversation

@idranme
Copy link
Collaborator

@idranme idranme commented Jan 1, 2026

Potential fix for https://github.com/LLOneBot/LuckyLilliaBot/security/code-scanning/73

General approach: Ensure that user input cannot arbitrarily control the destination host of the outgoing request. Enforce a strict allow-list on the hostname (using equality rather than substring matching), and also validate the scheme (only http/https) and optionally the port. All URL construction should be based on the parsed URL object rather than trusting an arbitrary string.

Best concrete fix here:

  1. Parse the incoming url once into a URL object.
  2. Validate:
    • parsedUrl.protocol is http: or https:.
    • parsedUrl.hostname is exactly one of the allowed hosts (using ===).
  3. After those validations, rebuild the final URL from the URL object (so we don’t keep concatenating unparsed strings).
  4. When appending rkey, use parsedUrl.searchParams rather than string concatenation; e.g., set/append rkey via parsedUrl.searchParams.set('rkey', rkey) and then take parsedUrl.toString() as the final URL.
  5. Use that validated finalUrl in fetch.

These changes are all within src/webui/BE/server.ts in the /api/webqq/image-proxy handler, around lines 860–895. No new imports are required, since URL is already used elsewhere in the file.

Suggested fixes powered by Copilot Autofix. Review carefully before merging.

由 Sourcery 提供的总结

通过在获取前严格验证并重建被代理的 URL,加固图像代理端点,防御 SSRF 攻击。

Bug 修复:

  • 通过强制使用精确主机名的允许列表,并将协议限制为 HTTP/HTTPS,防止图像代理中的服务器端请求伪造(SSRF)。

功能增强:

  • 在构造被代理的图像 URL 时,使用结构化的 URL 解析和变更(包括对 rkey 的处理),而不是使用字符串拼接。
Original summary in English

Summary by Sourcery

Harden the image proxy endpoint against SSRF by strictly validating and rebuilding proxied URLs before fetching.

Bug Fixes:

  • Prevent server-side request forgery in the image proxy by enforcing an allow-list of exact hostnames and restricting protocols to HTTP/HTTPS.

Enhancements:

  • Use structured URL parsing and mutation (including rkey handling) instead of string concatenation when constructing proxied image URLs.

…gery

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 1, 2026

审核者指南(在小型 PR 上折叠)

审核者指南

重构 /api/webqq/image-proxy 处理器,以执行严格的 URL 解析与校验(协议与主机名)、通过 URL API 操作查询参数,并确保仅使用已验证、重新构造的 URL 进行 fetch,从而减轻 SSRF 风险。

/api/webqq/image-proxy 中经过验证的图片代理请求的顺序图

sequenceDiagram
  actor User
  participant WebUIServer
  participant ntFileApi_rkeyManager
  participant QQImageHost

  User->>WebUIServer: HTTP GET /api/webqq/image-proxy?url=urlParam
  WebUIServer->>WebUIServer: decodedUrl = decodeURIComponent(urlParam)
  WebUIServer->>WebUIServer: parsedUrl = new URL(decodedUrl)
  WebUIServer->>WebUIServer: Validate protocol is http: or https:
  WebUIServer->>WebUIServer: Validate hostname in allowedHosts
  WebUIServer->>WebUIServer: Check parsedUrl.searchParams.has(rkey)
  alt Needs rkey and hostname is multimedia.nt.qq.com.cn or gchat.qpic.cn
    WebUIServer->>ntFileApi_rkeyManager: getRkey()
    ntFileApi_rkeyManager-->>WebUIServer: rkeyData
    WebUIServer->>WebUIServer: Select rkey by appid 1406 or 1407
    WebUIServer->>WebUIServer: parsedUrl.searchParams.set(rkey, rkey)
  end
  WebUIServer->>WebUIServer: finalUrl = parsedUrl.toString()
  WebUIServer->>QQImageHost: fetch(finalUrl)
  QQImageHost-->>WebUIServer: Image response
  WebUIServer-->>User: Proxied image response
Loading

/api/webqq/image-proxy 中 SSRF 安全的 URL 校验流程图

flowchart TD
  A["Start /api/webqq/image-proxy request"] --> B["Decode urlParam to decodedUrl"]
  B --> C["Try parse decodedUrl into URL parsedUrl"]
  C -->|parse error| R["Return 400 无效的URL"]
  C -->|ok| D{"protocol is http: or https:?"}
  D -->|no| S["Return 400 不支持的URL协议"]
  D -->|yes| E{"hostname in allowedHosts?"}
  E -->|no| T["Return 403 不允许代理此域名的图片"]
  E -->|yes| F{"parsedUrl.searchParams.has(rkey)?"}
  F -->|yes| H["Skip adding rkey"]
  F -->|no| G{"hostname is multimedia.nt.qq.com.cn or gchat.qpic.cn?"}
  G -->|no| H
  G -->|yes| I["appid = parsedUrl.searchParams.get(appid)"]
  I --> J{"appid in 1406 or 1407?"}
  J -->|no| H
  J -->|yes| K["rkeyData = ntFileApi.rkeyManager.getRkey()"]
  K --> L{"rkey exists?"}
  L -->|no| H
  L -->|yes| M["parsedUrl.searchParams.set(rkey, rkey)"]
  M --> H
  H --> N["finalUrl = parsedUrl.toString()"]
  N --> O["fetch(finalUrl) to image host"]
  O --> P["Return proxied image response"]
Loading

文件级变更

变更 详情 文件
在校验之前,使用稳定的变量名对传入的图片 URL 参数进行解码并记录日志。
  • 在执行 decodeURIComponent 之后,将之前可变的 url 字符串重命名为 decodedUrl
  • 调整日志输出,记录 decodedUrl 的值,而不是原始的 url 变量。
src/webui/BE/server.ts
加固 URL 解析和校验,限制向外部发起的请求只能访问特定的 http/https 主机,并避免使用基于子串的检查。
  • 将解码后的 URL 解析为一个 URL 实例,并在解析失败时返回 400 响应。
  • 添加协议校验,仅允许 http:https:,对其他 scheme 返回 400。
  • 将允许主机检查改为要求主机名完全相等,而不是进行子串匹配。
  • 在后续的所有检查中复用已解析的 URL 对象,而不是依赖原始字符串。
src/webui/BE/server.ts
通过 URLSearchParams 安全管理 rkey 查询参数,替代字符串拼接,并在 fetch 中使用重新构造且已验证的 URL。
  • 通过 parsedUrl.searchParams.has 检测缺失的 rkey,而不是在原始 URL 字符串中搜索。
  • rkey 注入逻辑限制为精确匹配主机名 multimedia.nt.qq.com.cngchat.qpic.cn
  • 在需要时通过 parsedUrl.searchParams.set 设置 rkey,而不是拼接到 URL 字符串中。
  • 引入 finalUrl,通过 parsedUrl.toString() 得到,并将其作为 fetch 的目标 URL。
src/webui/BE/server.ts

小贴士与命令

与 Sourcery 交互

  • 触发新的审核: 在 Pull Request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审核评论。
  • 从审核评论生成 GitHub Issue: 通过回复某条审核评论,请求 Sourcery 从该评论创建一个 Issue。你也可以在评论中回复 @sourcery-ai issue,将其转换为 Issue。
  • 生成 Pull Request 标题: 在 Pull Request 标题中任意位置写上 @sourcery-ai,即可随时生成标题。也可以在 Pull Request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 概要: 在 Pull Request 正文任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 概要。也可以在 Pull Request 中评论 @sourcery-ai summary 来(重新)生成概要。
  • 生成审核者指南: 在 Pull Request 中评论 @sourcery-ai guide,可随时(重新)生成审核者指南。
  • 解决所有 Sourcery 评论: 在 Pull Request 中评论 @sourcery-ai resolve,将标记所有 Sourcery 评论为已解决。如果你已经处理完所有评论且不希望再看到它们,这会很有用。
  • 撤销所有 Sourcery 审核: 在 Pull Request 中评论 @sourcery-ai dismiss,撤销所有现有的 Sourcery 审核。尤其适用于你希望从一次全新的审核开始时——别忘了再评论 @sourcery-ai review 来触发新的审核!

自定义你的使用体验

访问你的 控制面板 以:

  • 启用或禁用审核功能,例如 Sourcery 自动生成的 Pull Request 概要、审核者指南等。
  • 更改审核语言。
  • 添加、删除或编辑自定义审核指令。
  • 调整其他审核设置。

获取帮助

Original review guide in English
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Refactors the /api/webqq/image-proxy handler to perform strict URL parsing and validation (scheme and hostname), manipulate query parameters via the URL API, and ensure only a validated, reconstructed URL is used in fetch, mitigating SSRF risk.

Sequence diagram for validated image proxy fetch in /api/webqq/image-proxy

sequenceDiagram
  actor User
  participant WebUIServer
  participant ntFileApi_rkeyManager
  participant QQImageHost

  User->>WebUIServer: HTTP GET /api/webqq/image-proxy?url=urlParam
  WebUIServer->>WebUIServer: decodedUrl = decodeURIComponent(urlParam)
  WebUIServer->>WebUIServer: parsedUrl = new URL(decodedUrl)
  WebUIServer->>WebUIServer: Validate protocol is http: or https:
  WebUIServer->>WebUIServer: Validate hostname in allowedHosts
  WebUIServer->>WebUIServer: Check parsedUrl.searchParams.has(rkey)
  alt Needs rkey and hostname is multimedia.nt.qq.com.cn or gchat.qpic.cn
    WebUIServer->>ntFileApi_rkeyManager: getRkey()
    ntFileApi_rkeyManager-->>WebUIServer: rkeyData
    WebUIServer->>WebUIServer: Select rkey by appid 1406 or 1407
    WebUIServer->>WebUIServer: parsedUrl.searchParams.set(rkey, rkey)
  end
  WebUIServer->>WebUIServer: finalUrl = parsedUrl.toString()
  WebUIServer->>QQImageHost: fetch(finalUrl)
  QQImageHost-->>WebUIServer: Image response
  WebUIServer-->>User: Proxied image response
Loading

Flow diagram for SSRF-safe URL validation in /api/webqq/image-proxy

flowchart TD
  A["Start /api/webqq/image-proxy request"] --> B["Decode urlParam to decodedUrl"]
  B --> C["Try parse decodedUrl into URL parsedUrl"]
  C -->|parse error| R["Return 400 无效的URL"]
  C -->|ok| D{"protocol is http: or https:?"}
  D -->|no| S["Return 400 不支持的URL协议"]
  D -->|yes| E{"hostname in allowedHosts?"}
  E -->|no| T["Return 403 不允许代理此域名的图片"]
  E -->|yes| F{"parsedUrl.searchParams.has(rkey)?"}
  F -->|yes| H["Skip adding rkey"]
  F -->|no| G{"hostname is multimedia.nt.qq.com.cn or gchat.qpic.cn?"}
  G -->|no| H
  G -->|yes| I["appid = parsedUrl.searchParams.get(appid)"]
  I --> J{"appid in 1406 or 1407?"}
  J -->|no| H
  J -->|yes| K["rkeyData = ntFileApi.rkeyManager.getRkey()"]
  K --> L{"rkey exists?"}
  L -->|no| H
  L -->|yes| M["parsedUrl.searchParams.set(rkey, rkey)"]
  M --> H
  H --> N["finalUrl = parsedUrl.toString()"]
  N --> O["fetch(finalUrl) to image host"]
  O --> P["Return proxied image response"]
Loading

File-Level Changes

Change Details Files
Decode and log the incoming image URL parameter using a stable variable name before validation.
  • Rename the previously mutable url string to decodedUrl after decodeURIComponent.
  • Adjust logging to output the decodedUrl value instead of the original url variable.
src/webui/BE/server.ts
Harden URL parsing and validation to restrict outbound requests to specific http/https hosts and avoid substring-based checks.
  • Parse the decoded URL into a URL instance and handle parsing failures with a 400 response.
  • Add protocol validation to only allow http: and https:, returning 400 for other schemes.
  • Change the allowed host check to require exact hostname equality instead of substring matching.
  • Reuse the parsed URL object for all subsequent checks instead of relying on raw strings.
src/webui/BE/server.ts
Safely manage the rkey query parameter via URLSearchParams instead of string concatenation, and use the reconstructed, validated URL for fetch.
  • Determine missing rkey using parsedUrl.searchParams.has rather than searching the raw URL string.
  • Restrict rkey injection logic to exact hostnames for multimedia.nt.qq.com.cn and gchat.qpic.cn.
  • Set the rkey using parsedUrl.searchParams.set when appropriate instead of concatenating to the URL string.
  • Introduce finalUrl derived from parsedUrl.toString() and use it as the target for fetch.
src/webui/BE/server.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@idranme idranme changed the base branch from main to dev January 1, 2026 07:10
@idranme idranme changed the base branch from dev to main January 1, 2026 07:12
@idranme idranme changed the base branch from main to dev January 1, 2026 07:12
@idranme idranme closed this Jan 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants