Skip to content

[安全] LLM API Key 明文存储,且 base_url 可控(密钥泄露 / SSRF) #18

Open

Description

@yuezengwu

cc @wyzBelinda

问题

  • 用户的第三方 LLM api_key 被明文写入数据库,无任何加密或脱敏:
    • modules/RareSystem/backend/services/settings_service.py:79-95(create)
    • modules/RareSystem/backend/services/settings_service.py:118-119(update)
  • custom provider 允许调用方自定义 base_url(api/settings.py:28),LLM 客户端会向该地址发起出站请求,构成 SSRF 面。

影响

数据库文件一旦泄露或被越权读取,用户的 OpenAI/Anthropic 等密钥全部泄露(可产生真实费用与账号风险)。可控 base_url 可被用于探测内网(SSRF)。

解决方案

  1. 密钥加密存储(如 Fernet + 环境变量中的主密钥 / KMS),仅在调用时解密;确认任何接口都不回传明文 key。
  2. 对 base_url 做出站白名单校验,拒绝内网/回环地址。
  3. 结合认证与访问控制,限制谁能读写 LLM 设置。

优先级

中高。商业化 / 多用户阶段前解决。

Activity

  1. wen1112 commented on Jul 6, 2026

    @wen1112
    Collaborator
  2. FenjuFu commented on Jul 21, 2026

    @FenjuFu
    Contributor

    读了下相关代码,先补两条核实结果,再提方案。#15 / #17 我已经提了 PR(#29 / #30),这个想先对齐再动手。

    核实

    1. 「确认任何接口都不回传明文 key」这条目前是成立的 —— api/settings.py 的 LLMSettingsResponse 字段里没有 api_key,base_url 会返回但 key 不会。所以现在的暴露面是落库明文,不是接口泄露。这点值得写进回归测试固定住,否则以后有人给响应模型加字段就破功了。
    2. custom provider 的 base_url 走 Field(None, max_length=500),除长度外无任何校验,LLMClient 直接拿它发出站请求 —— SSRF 面确认存在。

    方案(三个决策点想请你们拍板)

    A. 加密方式

    倾向 Fernet + 主密钥从环境变量注入(cryptography 已是常见依赖),而不是直接上 KMS:本项目定位是可自托管的本地系统,强依赖云 KMS 会抬高部署门槛(和 #6「降低部署门槛」相冲)。

    具体做法:

    • LLM_SETTINGS_ENCRYPTION_KEY 环境变量提供主密钥,缺失时启动即失败而不是降级明文(fail closed)。
    • 密文列改名 api_key_encrypted,避免和历史明文列混淆。
    • 只在 LLMClient 构造时解密,不进日志、不进响应模型。

    想确认的:能否接受「必须配置一个环境变量才能启动」这个硬性要求?如果希望单机 demo 免配置,我可以做成「未配置时自动生成密钥文件到 data/ 并 chmod 600」,但那样安全性会打折,我个人不推荐。

    B. 存量数据迁移

    仓库已有 alembic(alembic/versions/0af50254bf91_add_skill_configs_table.py),所以我打算写一个 migration:新增 api_key_encrypted 列 → 读旧明文列逐行加密写入 → 清空旧列。

    想确认的:是直接删除旧 api_key 列,还是保留一个空列过渡一个版本?删掉更干净,但如果有人已经在生产跑了、又没备份,回滚会丢配置。

    C. SSRF 白名单的粒度

    三种做法,请选:

    1. 拒绝私网/回环目标(解析 hostname → 检查是否落在 RFC1918 / 127/8 / 169.254/16 / ::1 等),其余放行。灵活,但要注意 DNS rebinding:校验时解析到公网、真正请求时解析到内网。要挡住得在连接层固定 IP,实现较重。
    2. 显式白名单:LLM_ALLOWED_BASE_URLS 环境变量列出允许的 host,不在其中一律拒。最稳,但自托管用户每换一个模型服务就要改配置。
    3. 干脆去掉 custom provider,只保留内置 provider 列表。最安全,但会砍掉「接任意 OpenAI 兼容端点」的能力 —— 考虑到 modules/pixi_RAG-HPO 那边就是靠 RAG_HPO_BASE_URL 接 DeepSeek/Groq 的,这个能力应该是有人在用的,所以我倾向 2,把安全边界交给部署者显式声明。

    方向定了我就按这个做,加密/迁移/白名单可以拆成两个 PR(加密+迁移一个,SSRF 一个),方便分开审。也完全 OK 由你们自己来做 —— 那我就不动,只把上面的核实结果留在这。

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions