LIGHT CHASER 安全审计报告

24次阅读
没有评论

LIGHT CHASER 安全审计报告

0. 概述

项目 内容
审计对象 light-chaser(数据可视化大屏低代码平台)
代码仓库 /Users/h/Documents/02-tools/11-code/light-chaser
审计时间 2026-08-11
审计范围 backend(Spring Boot 3.2.5 / Java 17 / MyBatis Plus)+ frontend(React 18 / Vite / MobX)
审计目标 反序列化、命令执行 / RCE、SQL 注入、未授权访问、硬编码凭据、XSS、SSRF、越权等
总体结论 ⚠️ 存在多项可直接获取系统数据 / 内网权限的严重漏洞。系统完全无鉴权,且存在「任意 SQL 执行 + 硬编码密钥」的组合链,可导致整个应用数据库全量泄露(含数据源密码、AI 模型 API Key)。

最严重的一句话总结:应用没有任何登录/鉴权机制,所有 /api/** 接口匿名可访问;配合 /api/db/executor/execute 的任意 SELECT 执行接口和硬编码在源码里的 AES 密钥,远程未授权攻击者即可解密所有数据源密码、读取所有 AI API Key、读写全部业务数据,并可进一步通过数据源管理接口对内网发起 SSRF / 数据库连接攻击。


1. 漏洞汇总

# 严重程度 漏洞 影响 位置
C1 🔴 严重 全站无鉴权 / 未授权访问 所有接口匿名可访问 全局(pom 无 Spring Security,无任何过滤器)
C2 🔴 严重 任意 SQL 执行(数据全量窃取) 读取/枚举整个应用数据库及所有已配置数据源 DbExecutorController / DbExecutorServiceImpl
C3 🔴 严重 数据源密码加密体系失效(硬编码 AES 密钥 + 双 Base64 + 前端密钥明文传输 + 解密 oracle) 解密全部存储密码 application.yml / CryptoConfig / CryptoUtil / DebugController
C4 🔴 严重 DebugController 生产遗留后门(泄露 RSA 私钥 / 解密 oracle) 直接泄露私钥、任意密文解密 DebugController
C5 🔴 严重 数据源管理 = SSRF + 内网数据库连接 + 任意 SQLite 文件路径 探测/攻击内网,读取服务器 SQLite 文件 CommonDatasourceServiceImpl / DataBaseExecuteFactory
C6 🔴 严重 前端 eval 任意代码执行(存储型 XSS / 浏览器端代码执行) 任意打开恶意项目即执行任意 JS 前端多处 eval(见 3.6)
H1 🟠 高危 AI 模型管理 SSRF + API Key 明文存储 对内网任意地址发起可控 POST 并回显响应 AiModelServiceImpl.run
H2 🟠 高危 项目导入 Zip 解压写入 / 存储型攻击载荷落地 覆盖图片文件、落地 XSS 载荷 ProjectServiceImpl.importProject
H3 🟠 高危 敏感信息/内部细节泄露 泄露内网主机、端口、异常堆栈、密码明文日志 多处 e.getMessage()、DebugController、日志
M1 🟡 中危 无对象级授权(越权 / IDOR) 任意 id 可读写删任意资源 全部 CRUD 接口
M2 🟡 中危 文件上传仅校验扩展名(不校验内容/Magic Bytes) 上传 polyglot / 恶意内容 ImageServiceImpl / ProjectServiceImpl.uploadCover
M3 🟡 中危 静态资源目录任意文件读取(待验证) 潜在路径穿越读取服务器文件 StaticSourceInterceptor
M4 🟡 中危 敏感数据弱加密/明文存储 + 密钥文件权限 AI Key 明文、RSA 私钥落盘 ai_model 表 / keys/
L1 🟢 低危 依赖版本偏旧 / 潜在已知 CVE 供应链风险 pom.xml
L2 🟢 低危 日志打印敏感信息 日志泄露凭据 DebugController / AiModelServiceImpl
L3 🟢 低危 未捕获 NPE / 500 错误 轻微 DoS / 报错 ProjectServiceImpl.getProjectData

2. 详细发现

C1 🔴 全站无鉴权(未授权访问)— 根因漏洞

位置:全局。

  • backend/pom.xml 中未引入 Spring Security。
  • 全部 Controller 无任何鉴权注解,backend/src/main/java 中检索不到 SecurityFilterChain、OncePerRequestFilter、HandlerInterceptor(唯一的 StaticSourceInterceptor 是 WebMvcConfigurationSupport,只负责静态资源映射,不做鉴权)。
  • 前端 FetchUtil.ts 请求不带任何 Token / Session 凭据。

影响:所有 /api/project/**、/api/commonDatabase/**、/api/db/executor/**、/api/aiModel/**、/api/image/**、/api/crypto/**、/api/debug/** 均可匿名访问。它是 C2~C6、H1~H3 全部可利用的前提放大器。

修复:接入统一鉴权(JWT / Session / OAuth2 / 前端 + 网关),默认拒绝未登录访问;对「预览/查看项目」场景使用只读、带签名的临时令牌。


C2 🔴 任意 SQL 执行 → 应用数据库全量窃取

位置:controller/DbExecutorController.java:17 → service/impl/DbExecutorServiceImpl.java:32-53。

// DbExecutorServiceImpl.executeSql
String sql = Base64Util.decode(dbExecutorEntity.getSql());
if (!sql.trim().toLowerCase().startsWith("select"))
    throw new AppException(...);   // 仅校验“以 select 开头”
CommonDatasourcePO dataSourcePOCommon = commonDatasourceService.getById(dbExecutorEntity.getId());
...
JdbcTemplate jdbcTemplate = DataBaseExecuteFactory.buildDataSource(dataSourcePOCommon);
List<Map<String, Object>> res = jdbcTemplate.queryForList(sql);   // 直接执行客户端 SQL

利用分析:

  • sql 由客户端 base64 编码后传入,服务端仅校验 startsWith("select"),select 之后的任意内容都被原样执行。
  • id 由客户端指定,指向任何已存在的数据源(且无任何归属校验)。
  • 攻击者可通过 C3 所述方式创建一个指向主库 ./lc-server.db 的 SQLite 数据源(url=jdbc:sqlite:./lc-server.db),随后调用 /api/db/executor/execute 对整个应用数据库执行任意 SELECT,直接读取:
    • common_database 表 → 全部数据源用户名/URL/AES 加密的密码(配合 C3 可解密);
    • ai_model 表 → 全部 AI 模型 API Key 明文;
    • project / image 表 → 全部业务数据。
  • queryForList 仅能执行 SELECT,但 SELECT 已足以造成全量数据泄露;配合 SQLite PRAGMA table_info(...)、sqlite_master 可枚举所有表结构。

修复:① 该接口仅允许执行「项目内白名单 SQL」,禁止任意 SQL;② SQL 来源不可信时必须参数化并限制为只读 + 单条语句;③ 后端执行前校验 SQL 目标数据源与请求者权限;④ 对接口做鉴权(见 C1)。


C3 🔴 数据源密码加密体系完全失效(硬编码 AES 密钥)

位置:

  • backend/src/main/resources/application.yml:63
  • config/CryptoConfig.java:34,186(DEFAULT_AES_KEY)
  • util/RSAKeyGenerator.java:59
  • util/CryptoUtil.java(双 Base64 解码 + AES-CBC)
# application.yml
light-chaser:
  crypto:
    aes:
      key: YmVVaTJUNGdvV3huQnZXZzlEUE9kdnBaaS9ZcTZ0UzZtWXl2TWVXTzQ0Qz0=   # 硬编码 AES-256 密钥

关键事实(已验证):

  1. AES 密钥 YmVVaTJUNGdvV3huQnZXZzlEUE9kdnBaaS9ZcTZ0UzZtWXl2TWVXTzQ0Qz0= 解码第一层为 beUi2T4goWxnBvWg9DPOdvpZi/Yq6tS6mYyvMeWO44C=,再解码得到 32 字节的 AES-256 密钥,且与前端、源码、生成脚本完全一致,属于随发行版公开的固定密钥。
  2. 前端 DataSourceStore.encryptPassword 流程(frontend/src/pages/home/datasource/DataSourceStore.ts:151-178):
    • 前端随机生成 AES 密钥 aesKey → AES 加密密码;
    • 用 /api/crypto/public-key 返回的公钥 RSA 加密 AES(密码);
    • 请求 POST /api/commonDatabase/add,body 同时携带 password=RSA(AES(密码)) 与 aesKey=<明文AES密钥>。
  3. 后端 getEncryptedPassword(CommonDatasourceServiceImpl.java:84-96)用服务器 RSA 私钥解密 → 再用请求体里明文传入的 aesKey 解密 → 最后用硬编码后端 AES 密钥重新加密入库。

结论:这套“RSA + AES 双重加密”在没有提供任何机密性——解密所需的 AES 密钥本身就是随请求明文发送的,而落库用的 AES 密钥又硬编码在公开源码中。任何人拿到数据库即可解密所有密码;配合 C2 的任意 SQL,远程即可完成「读密文 → 硬编码密钥解密 → 获取内网数据库明文口令」的完整链路。

修复:① 移除源码中的固定 AES 密钥,改为部署时随机生成并经安全渠道下发;② 禁止在请求中明文传递解密密钥;③ 改用服务端持久的密钥管理(KMS / 环境变量 + 权限控制),数据源密码应以不可逆或受控可解密的方式存储,并最小化泄露面。


C4 🔴 DebugController:生产环境遗留调试后门

位置:controller/DebugController.java(/api/debug/**)。

// ① 生成并返回全新 RSA 私钥给任意调用者
@PostMapping("/generate-new-keypair")
public ApiResponse<KeyPairResponse> generateNewKeyPair() {
    KeyPair keyPair = CryptoUtil.generateRSAKeyPair();
    ...
    response.setPrivateKeyRaw(privateKey);        // ← 直接把私钥回给客户端!
    return ApiResponse.success(response);
}

// ② RSA 解密 oracle:任意密文用服务器 RSA 私钥解密后回显明文
@PostMapping("/test-rsa")
public ApiResponse<String> testRSA(@RequestBody TestRSARequest request) {
    String decrypted = CryptoUtil.decryptByRSAPrivateKey(request.getEncryptedData(), privateKey);
    return ApiResponse.success(decrypted);        // ← 解密结果回显
}

// ③ 完整加解密测试:回显最终明文密码
@PostMapping("/test-full-flow")
public ApiResponse<TestFlowResponse> testFullFlow(@RequestBody TestFlowRequest request) {
    ...
    response.setFinalPassword(finalPassword);      // ← 明文密码回显
    return ApiResponse.success(response);
}

影响:

  • generate-new-keypair 直接向任意匿名请求者下发 RSA 私钥,等同于向攻击者交付整套加密体系的解密能力;
  • test-rsa 是一个服务端 RSA 解密 oracle,可用来解出任何被服务器私钥包裹的密文(例如被加密的数据源密码中间态);
  • test-full-flow 直接回显“最终明文密码”,并会把 testPassword / aesKey 明文打印到服务端日志。

修复:彻底删除 /api/debug/** 相关 Controller(该类仅用于联调,绝不应出现在生产)。同时把 src/main/java/.../util/CryptoConfigTest.java 这类 main 方法调试类移出主源码或删除。


C5 🔴 数据源管理 = SSRF + 内网数据库连接 + 任意 SQLite 路径

位置:

  • controller/DataSourceController.java(/api/commonDatabase/add|update|test/{id})
  • service/impl/CommonDatasourceServiceImpl.java(addDataSource / testDataSourceConnect)
  • executor/DataBaseExecuteFactory.java:45-213(buildDataSource / normalizeJdbcUrl)

利用分析:

  1. 任意 JDBC URL:DatasourceAddRequest.url 完全由客户端控制,后端不校验目标主机/端口/协议。normalizeJdbcUrl 对以 jdbc: 开头的 URL 原样使用,否则按类型拼接。
  2. 连接测试触发真实连接:GET /api/commonDatabase/test/{id} 会对该 URL 发起真实数据库连接;/api/db/executor/execute 也会建立连接。这提供了:
    • 内网探测:任意创建 jdbc:mysql://192.168.x.x:3306/...、jdbc:postgresql://<内网IP>:5432/...、Oracle / SQLServer 等数据源并触发连接,即可对内网进行主机/端口扫描(连接结果与错误信息可作回显)。
    • SQLite 任意文件路径:jdbc:sqlite:/etc/xxx.db 或 ./lc-server.db 可指向服务器上任意 SQLite 文件,配合 C2 读取其内容。
    • JDBC 连接串注入面:MySQL 驱动 URL 支持 allowLoadLocalInfile、autoDeserialize 等参数,历史上可组合出任意文件读取/反序列化攻击;当前依赖 mysql-connector-java 8.0.33 默认已关闭多数危险特性,但 URL 由攻击者完全控制,风险应评估并重点缓解。
  3. 错误信息泄露:buildUserFriendlyErrorMessage 将 exception.getMessage() 拼入回显(CommonDatasourceServiceImpl.java:222-240),泄露目标端口/主机/认证细节。

修复:① 数据源 URL 增加协议与目标地址白名单/黑名单(禁止私网/回环地址或提供代理出口);② 连接测试接口做超时、限速、鉴权;③ 对 SQLite 路径做严格限制,禁止指向应用主库与绝对路径;④ 收敛错误信息,不向客户端回显原始异常。


C6 🔴 前端 eval 任意代码执行(存储型 XSS / 浏览器端代码执行)

位置(全部由 dataJson / 组件配置驱动,而 dataJson 可通过未授权接口写入):

文件 行号 触发内容
framework/core/AbstractDesignerController.ts 56, 91 数据过滤 filter,数据刷新时执行
comps/common-component/data-config/database/DatabaseConfig.tsx 77 数据库数据过滤
comps/common-component/data-config/api/ApiDataConfig.tsx 213 API 数据过滤
comps/map/AMapComponent.tsx 59 高德地图 customCode
comps/antd/g2plot-custom/G2PlotCustomController.ts 35, 75 图表 customCode
designer/blueprint/node/core/impl/logical-process/BPLogicalProcessNodeController.ts 42 蓝图逻辑处理 handler
designer/blueprint/node/core/impl/condition/BPConditionNodeController.ts 45 蓝图条件判断 handler
json-schema/LCGUI.tsx 104 配置规则
// AbstractDesignerController.ts:56
const func = eval(`(${filter})`);        // filter 来自项目 dataJson 组件配置
res = typeof func === 'function' ? func(res) : res;

利用链:

  1. 攻击者(匿名)调用 POST /api/project/create / update 写入含恶意 filter/customCode/handler 的 dataJson,或直接 POST /api/project/importProject 上传恶意项目;
  2. 诱导受害者访问公开路由 /view?id=<恶意项目id>(pages/view/DesignerViewPage.tsx + router/MainRouter.tsx),或直接在 /designer 中打开该项目;
  3. ViewDesignerLoader 解析 dataJson 并实例化组件 → 数据加载/蓝图执行时触发 eval → 在应用源上执行任意 JS(窃取同源数据、操作同源接口、植入恶意弹窗、在部署于内网/企业门户时横向利用受害者浏览器上下文)。

修复:① 业务层改为使用 函数式沙箱(如 new Function 加参数白名单、或 @lezer/ts 语法解析 + 受限解释执行、或 Web Worker + CSP unsafe-eval 禁止),不要直接 eval 不可信配置;② dataJson 存储前做序列化校验与白名单字段过滤;③ 全局 CSP 设置 script-src 'self',禁用 eval;④ 对创建/导入项目的接口加鉴权并限制来源。


H1 🟠 AI 模型管理 SSRF + API Key 明文存储

位置:service/impl/AiModelServiceImpl.java(createModel:116 / updateModel:130 / run:197-290)。

  • /api/aiModel/create / update 允许匿名设置任意 baseUrl、chatCompletionsPath、apiKeyHeader、apiKeyPrefix、apiKey(存库为明文)。
  • /api/aiModel/run 用 RestTemplate 对 baseUrl + path 发起 POST,请求头由攻击者自定义,并把完整响应体(rawResponse)回显给客户端(AiModelServiceImpl.java:288)。
  • 即:攻击者可把 baseUrl 指向内网任意地址(如云元数据 http://169.254.169.254、内网 HTTP 服务),经 /api/aiModel/run 发起带认证头的可控 POST 并回读响应 → SSRF(服务端请求伪造,可回显)。
  • apiKey 明文存于 ai_model 表,配合 C2 可被任意读取;LC_AI_API_KEY 环境变量同样进入该表。

修复:① baseUrl 增加协议与地址白名单(仅允许 HTTPS 公共地址/配置的代理);② run 接口鉴权 + 限流;③ API Key 加密存储(KMS / 服务端加密),接口永不回显原始 Key;④ 回显响应体前做脱敏与大小限制。


H2 🟠 项目导入 Zip 解压写入 / 攻击载荷落地

位置:service/impl/ProjectServiceImpl.java:283-369(importProject)。

  • 上传的 zip 会被解压并把条目写入 imageAbsolutPath/<basename>(REPLACE_EXISTING,可覆盖已有图片)。
  • entryName.contains(imagePath) 后取 lastIndexOf("/")+1 的 basename 在一定程度上缓解了 ../ 穿越,但写入内容不校验——攻击者可构造内容为 HTML/JS 的“图片”落地到静态目录。
  • 导入的 .lc 项目 JSON 原样入库为 project.data_json(ProjectServiceImpl.java:354-357),是 C6 存储型 XSS 的最便捷投递通道。

修复:① 解压前校验每个条目 basename 与目标目录(拒绝 ..、绝对路径、符号链接);② 只允许白名单扩展名且校验 Magic Bytes;③ 限制 zip 条目数与总大小(zip bomb 防护);④ 对 .lc 内容做 schema 校验与无害化。


H3 🟠 敏感信息 / 内部细节泄露

  • 数据源连接测试 / SQL 执行 / AI 调用失败时,多处把 exception.getMessage() 或远端错误原文拼入 HTTP 响应(CommonDatasourceServiceImpl.java:238、AiModelServiceImpl.java:263,266)。
  • DebugController.testFullFlow 把 testPassword、aesKey 明文写入日志(DebugController.java:97-99);DataBaseExecuteFactory 日志打印完整 url;AiModelServiceImpl 日志打印 endpoint。
  • GlobalExceptionHandler.java:25 对非自定义异常统一回“服务器端异常”,但 AppException 分支会把内部 message 透出。

修复:统一异常出口并脱敏;日志避免打印明文凭据与完整 URL;生产环境关闭 debug 接口与堆栈输出。


M1 🟡 无对象级授权(越权 / IDOR)

位置:全部以数字 id 为操作键的接口:

  • 项目:/api/project/getProjectData/{id}、getProjectInfo/{id}、update、del/{id}、copy/{id}、cover、importProject、exportProject;
  • 数据源:/api/commonDatabase/get|copy|test/{id}、update、batchDel;
  • 图片:/api/image/batchDelete;
  • AI 模型:/api/aiModel/get|copy/{id}、update、batchDel。

无任何“数据归属/角色”概念。当前因 C1(无鉴权)影响被放大;将来引入鉴权后仍需补充对象级权限校验。


M2 🟡 文件上传仅校验扩展名

位置:ImageServiceImpl.java:43-50、ProjectServiceImpl.java:122-127。

  • 只校验后缀在 {.bmp,.jpg,.jpeg,.png,.gif} 白名单内,不校验 Magic Bytes / 内容,可上传 polyglot 文件。
  • 原始文件名存库(image.name),并在管理列表中原样展示,存在存储型 XSS 的潜在反射点(需配合渲染方式确认)。

修复:校验文件头(Magic Bytes)+ 服务端重新编码;展示时转义。


M3 🟡 静态资源目录文件读取(待验证)

位置:config/StaticSourceInterceptor.java:20-28。

  • /static/images/**、/static/covers/** 直接映射到磁盘目录。Spring 默认 PathResourceResolver 会拦截 .. 穿越,大概率安全,但建议验证编码变体(..%2f、%2e%2e%2f)并加目录边界校验。

M4 🟡 敏感数据弱加密 / 明文存储 / 密钥文件权限

  • ai_model.api_key 明文存储(见 H1)。
  • common_database.password 使用公开固定密钥加密(见 C3)。
  • RSA 私钥落盘到 keys/private_key.pem(项目资源目录),未设置文件权限约束;若该目录被 Web 服务读取(如反向代理配置不当)即泄露。

L1 🟢 依赖版本偏旧

  • Spring Boot 3.2.5、fastjson2 2.0.56、sqlite-jdbc 3.42.0.0、mysql-connector-java 8.0.33、postgresql 42.6.0、ojdbc8 21.9.0.0、mssql-jdbc 12.2.0。
  • 反序列化面:fastjson2 在项目中的用法均为显式目标类(如 ProjectDependenciesDTO),未启用 autoType,暂未见可直接 RCE 的入口,但应升级至最新补丁版本以覆盖已知 CVE。

L2 🟢 日志打印敏感信息

见 H3。建议对日志脱敏(密码、API Key、完整 URL、异常堆栈默认不落盘或脱敏)。

L3 🟢 空指针 / 500 未兜底

  • ProjectServiceImpl.getProjectData:selectOne(queryWrapper).getDataJson() 在项目不存在时抛 NPE(ProjectServiceImpl.java:69)。
  • GlobalExceptionHandler 对未知异常仅返回通用文案,但 AppException 分支会透出内部 message。

3. 关键攻击链(PoC 思路)

攻击链 A:应用数据库全量泄露(未授权)

1. GET  /api/crypto/public-key                    → 获得服务器 RSA 公钥
2. 本地:生成随机 AES 密钥 K,AES 加密任意密码 P → C1
        RSA(公钥) 加密 C1 → C2
3. POST /api/commonDatabase/add
        { type: SQLite, url: "jdbc:sqlite:./lc-server.db", password: C2, aesKey: K }
4. POST /api/db/executor/execute
        { id: <第3步返回的数据源id>,
          sql: base64("select * from common_database") }
   → 得到全部数据源用户名/URL/AES 密文密码
5. 用硬编码 AES 密钥(源码中的 DEFAULT_AES_KEY)解密 → 内网数据库明文口令
6. 同理执行 select * from ai_model → 全部 AI API Key 明文

攻击链 B:SSRF → 内网 / 云元数据

1. POST /api/aiModel/create
   { name: "x", baseUrl: "http://169.254.169.254/latest/meta-data/", chatCompletionsPath: "iam/security-credentials/", apiKey: "x", ... }
2. POST /api/aiModel/run
   { scene: "style-optimize", prompt: "x", modelId: <新模型id> }
   → 响应中回显云元数据 / 内网响应内容

攻击链 C:存储型 XSS / 浏览器端代码执行

1. POST /api/project/create 构造含恶意 filter/customCode 的 dataJson(或 importProject 上传恶意 .lc)
2. 诱导受害者访问 https://<host>/view?id=<恶意项目id>
3. 组件渲染/数据加载 → eval("恶意代码") → 在应用源执行任意 JS

4. 修复优先级建议

优先级 动作
P0(立即) ① 删除 /api/debug/** 与 CryptoConfigTest;② 为所有 /api/** 增加统一鉴权;③ 下线/收紧 /api/db/executor/execute,禁止任意 SQL;④ 移除源码硬编码 AES 密钥,改为部署期随机生成 + 安全下发;
P0 ⑤ 数据源 URL / AI baseUrl 白名单化,阻断 SSRF;⑥ 项目导入做 Zip 安全解压 + .lc schema 校验;
P1 ⑦ 前端去掉业务 eval,改为沙箱/受限解释执行,加 CSP;⑧ AI API Key、数据源密码改加密存储 + 不落明文;⑨ 异常信息脱敏;
P1 ⑩ 补充对象级权限(归属校验);上传做 Magic Bytes 校验;
P2 ⑪ 升级依赖(Spring Boot / fastjson2 / 各 JDBC 驱动);⑫ 日志脱敏;⑬ 密钥文件权限加固;

5. 审计范围与方法

  • 审计方法:人工代码走查(backend 全部 Java 源码 5574 行 + frontend 关键渲染/请求链路)+ 危险模式检索(eval、dangerouslySetInnerHTML、innerHTML、startsWith("select")、硬编码密钥、无鉴权配置等)。
  • 覆盖文件:backend controller/*、service/impl/*、executor/*、config/*、util/*、global/*、application.yml、migration SQL、pom.xml;frontend pages/view、designer/loader、framework/core、comps/map|antd|common-component、pages/home/datasource|ai-model-management、utils/FetchUtil|CryptoUtil。
  • 说明:本次为静态代码审计,未进行动态利用验证;部分结论(M3、JDBC URL 反序列化)标注为“待验证/需评估”,建议在受控环境复测。
正文完
 0
评论(没有评论)