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 已足以造成全量数据泄露;配合 SQLitePRAGMA table_info(...)、sqlite_master可枚举所有表结构。
修复:① 该接口仅允许执行「项目内白名单 SQL」,禁止任意 SQL;② SQL 来源不可信时必须参数化并限制为只读 + 单条语句;③ 后端执行前校验 SQL 目标数据源与请求者权限;④ 对接口做鉴权(见 C1)。
C3 🔴 数据源密码加密体系完全失效(硬编码 AES 密钥)
位置:
backend/src/main/resources/application.yml:63config/CryptoConfig.java:34,186(DEFAULT_AES_KEY)util/RSAKeyGenerator.java:59util/CryptoUtil.java(双 Base64 解码 + AES-CBC)
# application.yml
light-chaser:
crypto:
aes:
key: YmVVaTJUNGdvV3huQnZXZzlEUE9kdnBaaS9ZcTZ0UzZtWXl2TWVXTzQ0Qz0= # 硬编码 AES-256 密钥
关键事实(已验证):
- AES 密钥
YmVVaTJUNGdvV3huQnZXZzlEUE9kdnBaaS9ZcTZ0UzZtWXl2TWVXTzQ0Qz0=解码第一层为beUi2T4goWxnBvWg9DPOdvpZi/Yq6tS6mYyvMeWO44C=,再解码得到 32 字节的 AES-256 密钥,且与前端、源码、生成脚本完全一致,属于随发行版公开的固定密钥。 - 前端
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密钥>。
- 前端随机生成 AES 密钥
- 后端
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)
利用分析:
- 任意 JDBC URL:
DatasourceAddRequest.url完全由客户端控制,后端不校验目标主机/端口/协议。normalizeJdbcUrl对以jdbc:开头的 URL 原样使用,否则按类型拼接。 - 连接测试触发真实连接:
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 由攻击者完全控制,风险应评估并重点缓解。
- 内网探测:任意创建
- 错误信息泄露:
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;
利用链:
- 攻击者(匿名)调用
POST /api/project/create/update写入含恶意filter/customCode/handler的dataJson,或直接POST /api/project/importProject上传恶意项目; - 诱导受害者访问公开路由
/view?id=<恶意项目id>(pages/view/DesignerViewPage.tsx+router/MainRouter.tsx),或直接在/designer中打开该项目; 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;frontendpages/view、designer/loader、framework/core、comps/map|antd|common-component、pages/home/datasource|ai-model-management、utils/FetchUtil|CryptoUtil。 - 说明:本次为静态代码审计,未进行动态利用验证;部分结论(M3、JDBC URL 反序列化)标注为“待验证/需评估”,建议在受控环境复测。