iYqueCode(源雀 SCRM)应用安全审计报告
审计方式:静态代码审计(未运行应用),基于当前
master分支快照。
审计目标:发现真实可利用的安全缺陷,给出修复优先级与代码级修复方向。
第一阶段:代码理解摘要
1. 应用定位与核心业务
源雀 SCRM(iYqueCode) 是一套基于企业微信的私域客户营销/客服系统,面向企业运营、销售、客服团队,核心能力包括:
- 员工活码、获客外链、智能群码、客户公海
- 群发营销(客户/客群群发)
- AI 客服 / AI 知识库(RAG + Milvus)
- 会话存档、AI 会话预审、热词分析
- 投诉管理、素材/话术、H5 营销页
2. 技术栈与关键依赖
| 层级 | 技术/版本 | 备注 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.3 + JDK 17 | |
| ORM | Spring Data JPA + MyBatis-Plus 3.4.0 | MyBatis XML 中未发现 ${} |
| 数据库/缓存 | MySQL 8.0.33 + Redis | 连接凭据明文配置 |
| 企业微信 SDK | wxJava weixin-java-cp 4.7.6.B |
|
| AI | LangChain4j 1.0.0-beta3 + 智谱 GLM-4/embedding-3 | API Key 硬编码 |
| 向量库 | Milvus 2.5.6 | |
| 认证 | JWT(jjwt 0.12.6,HS256) | 密钥硬编码 |
| 前端 | PC:Vue 3 + Vite + Element Plus;Mobile:Vue 3 + Vant | |
| 文档解析 | Apache POI(经 EasyExcel 传递)、PDFBox | 未显式禁用外部实体 |
3. 架构分层
HTTP 入口(31 个 Controller)
↓
Service / 异步/定时任务 / mass.sender / chain(RAG)
↓
JPA Dao / MyBatis-Plus Mapper
↓
MySQL / Redis / Milvus / 企业微信 API / 智谱 AI API
4. 入口点清单
- REST Controller:
IYqueLoginController、IYqueConfigController、IYqueGroupMsgController、IYqueMaterialController、IYqueAiController、FileController、IYcallbackController等 31 个。 - 定时任务:
QuartzJob.jobIdToMomentId(每小时)。 - 异步任务:多处
@Async。 - 消息消费者:无 MQ;企业微信推送由
/iycallback/handle消费。 - 拦截器:
JwtInterceptor(JWT 校验)、ReadOnlyInterceptor(演示模式只读)。
5. 关键数据流示例
- 登录:
IYqueLoginController.login→ 与配置中的明文密码比对 →JwtUtils.generateToken→ 返回 JWT。 - 文件上传/预览:
FileController.upload/openUpload→FileUtils扩展名校验 → 写入./upload→ 返回fileViewUrl;FileController.readFile直接拼接filename返回文件。 - 群发:
IYqueGroupMsgController.buildGroupMsg→MassSenderFactory→CustomerMassSender/GroupMassSender→ 调用WxCpService发送,附件通过FileUtils.downloadImage下载。
6. 已有防护机制
- JWT 统一拦截,部分路径白名单(
JwtConfig.java:21-23)。 - 登录限流
@RateLimit(仅/iYqueSys/login)。 - 演示模式只读拦截器(
demo=true时生效)。 - 企微回调使用 SHA1 + AES-CBC + PKCS7 解密。
- CORS 配置为
allowedOriginPatterns("*")+allowCredentials(true)。
重大缺失:没有角色/权限模型,无 @PreAuthorize、@Secured 或自定义鉴权;只要 token 有效即可调用所有管理接口。
第二阶段:威胁建模与审计计划
攻击者视角
对 SCRM 系统而言,最有价值的目标是:
- 企业微信配置密钥(corpId、agentSecret、token、encodingAESKey、会话存档私钥)。
- 管理员 JWT / 管理员账号。
- 系统上的客户数据、会话存档。
- 服务器内网访问能力(SSRF / 任意文件读取)。
攻击面优先级 Top 5
- 认证绕过:硬编码 JWT 密钥 + 前端硬编码 token → 可直接获得后台全部权限。
- 未授权文件相关接口:
/file/fileView/**路径遍历、/file/openUpload任意文件上传。 - 未授权 SSRF:
/iYqueSys/uploadMediaId让服务端拉取任意 URL。 - 授权缺失导致的越权/敏感信息泄露:
/iYqueConfig/findIYqueConfig返回全部企微密钥;所有管理接口无角色隔离。 - 服务端本地文件读取/SSRF:
FileUtils.downloadImage非 HTTP(S) 分支拼接本地路径,多处业务入口调用。
审计计划
按优先级依次深挖:
- 认证/授权与 JWT 实现。
- 文件上传、下载、预览链路。
- SSRF / URL 拉取链路。
- 配置与凭据泄露。
- XSS / 文档解析 XXE / 其他注入类查漏补缺。
第三阶段 & 第四阶段:审计发现与风险定级
| 编号 | 漏洞 | 严重 | 置信度 | OWASP / CWE |
|---|---|---|---|---|
| 1 | 硬编码 JWT 密钥 + 前端硬编码有效 token,可伪造管理员 JWT | 严重 | 确认 | A07 / CWE-798, CWE-321 |
| 2 | 无访问控制/角色隔离,登录即拥有全部后台权限 | 严重 | 确认 | A01 / CWE-306 |
| 3 | /iYqueConfig/findIYqueConfig 泄露企微全部密钥 |
严重 | 确认 | A01,A02 / CWE-200 |
| 4 | /file/fileView/{filename} 未授权路径遍历任意文件读取 |
严重 | 确认 | A01 / CWE-22 |
| 5 | /iYqueSys/uploadMediaId 未授权 SSRF |
严重 | 确认 | A10 / CWE-918 |
| 6 | /file/openUpload 未认证任意文件上传 |
高危 | 确认 | A01,A04 / CWE-434 |
| 7 | FileUtils.downloadImage 本地路径遍历 + SSRF(群发/应用/机器人/客服/活码多处触发) |
高危 | 确认 | A10,A01 / CWE-22,CWE-918 |
| 8 | CORS 配置 * + allowCredentials(true) |
高危 | 确认 | A05,A07 / CWE-942,CWE-346 |
| 9 | 默认管理员账号密码硬编码且明文比较 | 高危 | 确认 | A07 / CWE-798,CWE-256 |
| 10 | application.yml 明文存放 DB/Redis/AI API Key 凭据 | 高危 | 确认 | A02 / CWE-798 |
| 11 | 企微配置 secrets 明文存储于数据库 | 高危 | 确认 | A02 / CWE-312 |
| 12 | 前端 v-html 渲染后端内容导致存储型 XSS |
中危 | 确认 | A03 / CWE-79 |
| 13 | Office/Excel 文档解析未显式禁用外部实体,存在 XXE/SSRF 风险 | 中危 | 疑似 | A08,A10 / CWE-611,CWE-918 |
| 14 | 生产环境 SQL 打印 + 多处 e.printStackTrace() + 异常信息返回 |
中危 | 确认 | A04,A09 / CWE-209,CWE-532 |
| 15 | 登录限流基于 getRemoteAddr 且无验证码,可被代理绕过 |
中危 | 确认 | A07 / CWE-307 |
| 16 | /iYqueSys/getAgentTicket 未校验 URL;/iYqueSys/weComRedirect 可生成任意 redirect 的 OAuth URL |
低危 | 确认 | A04 / CWE-20 |
SQL 注入 / 命令注入 / 反序列化 / SpEL / OGNL / LDAP / NoSQL 注入:代码中未发现字符串拼接 SQL、
${}、命令执行、ObjectInputStream.readObject、Fastjson 危险反序列化、SpEL 解析等。均标注为 未发现/不适用。
1. 硬编码 JWT 密钥 + 前端硬编码有效 token(严重)
位置
src/main/java/cn/iyque/utils/JwtUtils.java:13frontEnd/mobile/src/config.js:11-12src/main/java/cn/iyque/controller/IYqueLoginController.java:54-60
代码
private static final String SECRET_KEY = "iYqueSecretKeyForJwtTokenGenerationAndValidation";
TOKEN:
'eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJpeXF1ZSIsImlhdCI6MTc3MjUwMzkyOSwiZXhwIjoxNzczMTA4NzI5fQ.sslN7K_nyc9QVOiB3IhPNSyQuycqtYbMt7mwZXTJFINtRPOIZHJgIYCsI8YVfYrz',
验证
使用 iYqueSecretKeyForJwtTokenGenerationAndValidation 对 HMAC-SHA384 进行校验,签名 匹配。该 token 当前已过期,但攻击者可使用同一密钥签发新的 HS256/HS384 token,sub 设为 iyque 即可通过 JwtInterceptor。
影响
任何拿到源码/字节码/前端构建产物的人都能伪造管理员 JWT,完全绕过登录。
修复
- 密钥从环境变量/配置中心/Vault 注入,长度 ≥ 256 bit,随机生成。
- 禁止在前端硬编码 token。
- 上线前轮换密钥,旧 token 加入黑名单或缩短有效期并启用 refresh token。
2. 无访问控制/角色隔离(严重)
位置
- 全工程未使用
@PreAuthorize、@Secured、hasRole或自定义权限注解。 src/main/java/cn/iyque/interceptor/JwtInterceptor.java:22-35仅校验 token 是否有效。
代码
if(!JwtUtils.validateToken(token)){
sendJsonErrorResponse(response,HttpStatus.RE_LOGIN, "token失效重新登录");
return false;
}
影响
只要持有有效 JWT(包括伪造的),即可调用 /iYqueConfig/**、/iYqueUser/**、/groupMsg/buildGroupMsg、/material/saveOrUpdate 等全部管理接口;普通员工账号与管理员权限无差别。
修复
- 引入 RBAC:用户-角色-权限表。
- 在敏感接口加
@PreAuthorize("hasRole('ADMIN')")或自定义注解。 - 对业务数据操作做数据归属校验(如
createBy、corpId)。
3. /iYqueConfig/findIYqueConfig 泄露企微全部密钥(严重)
位置
src/main/java/cn/iyque/controller/IYqueConfigController.java:30-35src/main/java/cn/iyque/entity/IYqueConfig.java:26-48
代码
@GetMapping("/findIYqueConfig")
public ResponseResult<IYqueConfig> findIYqueConfig(){
return new ResponseResult<>(iYqueConfigService.findIYqueConfig());
}
返回字段包括 corpId、agentId、agentSecert、token、encodingAESKey、msgAuditPriKey、msgAuditSecret。
影响
配合 JWT 伪造,攻击者可一键获取企业微信全部凭证,进而解密回调、伪造企业微信消息、拉取会话存档。
修复
- 返回 DTO,剔除敏感字段。
- 将
saveOrUpdate接口仅限管理员。 - 数据库中加密存储 secrets(AES-256-GCM + KMS)。
4. /file/fileView/{filename} 未授权路径遍历任意文件读取(严重)
位置
src/main/java/cn/iyque/controller/FileController.java:183-209src/main/java/cn/iyque/config/JwtConfig.java:21
代码
@GetMapping("/file/fileView/{filename}")
public ResponseEntity<Resource> readFile(@PathVariable String filename) {
Path filePath = Paths.get(iYqueParamConfig.getUploadDir(), filename);
Resource resource = new UrlResource(filePath.toUri());
触发路径
/file/fileView/** 在 JWT 白名单 → 请求 GET /file/fileView/..%2F..%2F..%2Fetc%2Fpasswd → Paths.get("./upload", "../../../etc/passwd") → 读取系统任意文件。
影响
未认证即可读取服务器上应用进程有权限的任何文件(application.yml、数据库文件、日志、私钥等)。
修复
Path base = Paths.get(uploadDir).toAbsolutePath().normalize();
Path target = base.resolve(filename).normalize();
if (!target.startsWith(base)) throw new SecurityException();
5. /iYqueSys/uploadMediaId 未授权 SSRF(严重)
位置
src/main/java/cn/iyque/controller/IYqueLoginController.java:152-159src/main/java/cn/iyque/config/JwtConfig.java:23
代码
@GetMapping("/uploadMediaId")
public ResponseResult uploadMediaId(String url, String type, String name){
WxMediaUploadResult uploadResult = iYqueConfigService.findWxcpservice().getMediaService()
.upload(type, name + "." + url.substring(url.lastIndexOf(".") + 1, url.length()), url);
影响
未认证即可让服务端向任意 url 发起 HTTP 请求(wxJava 会先下载该 URL),用于探测内网、访问云 metadata(如 http://169.254.169.254/)、消耗企微 API 配额。
修复
- 从 JWT 白名单移除;仅管理员可用。
- 对
url做白名单校验(限定业务域名/CDN),禁止内网 IP、metadata IP、私有网段。
6. /file/openUpload 未认证任意文件上传(高危)
位置
src/main/java/cn/iyque/controller/FileController.java:126-180src/main/java/cn/iyque/config/JwtConfig.java:22
代码
@PostMapping("/openUpload")
public ResponseResult<String> openUpload(@RequestParam("file") MultipartFile file) {
String fileExtension = originalFileName.substring(originalFileName.lastIndexOf('.'));
...
String fileName = UUID.randomUUID()+fileExtension;
Path targetPath = Paths.get(iYqueParamConfig.getUploadDir(), fileName);
Files.write(targetPath, file.getBytes());
影响
- 未认证即可上传任意扩展名文件(HTML、SVG、XML、JS 等),并通过
/file/fileView/{uuid}.html分发。 - 与路径遍历不同,文件名是 UUID,无法直接遍历,但可作为钓鱼/恶意文件托管。
修复
- 从 JWT 白名单移除,或至少要求 JWT。
- 增加服务端 magic bytes 校验、限制扩展名白名单、限制 Content-Type。
- 上传目录不可执行,返回文件名不可预测。
7. FileUtils.downloadImage 本地路径遍历 + SSRF(高危)
位置
src/main/java/cn/iyque/utils/FileUtils.java:88-117
代码
public static File downloadImage(String imageUrl) {
if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")){
URL url = new URL(imageUrl);
ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream());
...
}else{
File localFile = new File(localUploadDir, imageUrl);
if (localFile.exists()) { return localFile; }
}
}
调用链(用户可控 URL)
- 群发:
ImageAttachmentConverter.java:23,46、FileAttachmentConverter.java:22、VideoAttachmentConverter.java:25,48、LinkAttachmentConverter.java:56 - 应用消息:
AgentMsgDto.java:39 - 群机器人:
RobotMsgDto.java:159 - 客服头像/转接二维码:
IYqueKfServiceImpl.java:404,636 - 员工活码 logo:
IYqueUserCodeServiceImpl.java:129,241
影响
- HTTP(S) 分支:SSRF,让服务端拉取外网/内网资源。
- 非 HTTP(S) 分支:路径遍历读取本地文件(如
../../../etc/passwd)。 - 由于 JWT 可伪造,这些“认证后”接口实际上可被未认证攻击者调用。
修复
// 拒绝 file://、ftp://、 jar:// 等 scheme
URL url = new URL(imageUrl);
if (!"http".equals(url.getProtocol()) && !"https".equals(url.getProtocol())) throw ...
// 本地分支:基目录校验
Path base = Paths.get(uploadDir).toAbsolutePath().normalize();
Path target = base.resolve(imageUrl).normalize();
if (!target.startsWith(base)) throw new SecurityException();
8. CORS 配置 * + allowCredentials(true)(高危)
位置
src/main/java/cn/iyque/config/CORSConfig.java:21-25
代码
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true);
影响
任意恶意网站可携带 Authorization 头发起跨域请求,配合泄露/伪造的 JWT 可直接调用后端所有接口。
修复
- 显式列出允许的域名,生产环境禁用
*。 - 若必须支持多域名,使用动态 origin 校验并禁止
null/IP/任意子域。
9. 默认管理员账号密码硬编码且明文比较(高危)
位置
src/main/resources/application.yml:27-28src/main/java/cn/iyque/controller/IYqueLoginController.java:54-55
代码
if(iYqueParamConfig.getUserName().equals(iQyqueAuthInfo.getUsername())
&&iYqueParamConfig.getPwd().equals(iQyqueAuthInfo.getPassword())){
影响
默认账号 iyque / iyque.cn 无法通过界面修改;密码无盐无哈希,配置泄露即账号泄露。
修复
- 首次启动强制修改密码;密码使用 BCrypt/Argon2 加盐哈希。
- 从 Vault/环境变量读取管理员凭据。
10. application.yml 明文存放 DB/Redis/AI 凭据(高危)
位置
src/main/resources/application.yml:9,22,47-48,60-63
代码
ai:
models:
configs:
glm-4:
api-key: aac7e6c6...
spring:
datasource:
username: root
password: root
redis:
password: (空或明文)
影响
源码/构建产物泄露即可直接连数据库、Redis、调用智谱 AI 接口,造成数据泄露与资费损失。
修复
- 凭据全部迁移至环境变量或配置中心。
- 数据库账号禁用 root,使用最小权限账号。
11. 企微配置 secrets 明文存储于数据库(高危)
位置
src/main/java/cn/iyque/entity/IYqueConfig.java:26-48src/main/java/cn/iyque/entity/IYqueAgent.java:41
影响
数据库备份、SQL 日志、越权查询均可直接拿到 agentSecert、encodingAESKey、msgAuditPriKey。
修复
- 入库前 AES-256-GCM 加密,密钥托管于 KMS/环境变量。
- 查询接口返回 DTO 不携带密文字段。
12. 前端 v-html 渲染后端内容导致存储型 XSS(中危)
位置
frontEnd/mobile/src/views/chat/detail.vue:16frontEnd/pc/src/components/AiChat.vue:85(renderMessage中对 JSON recommendations 的 title/reason/path 未编码)frontEnd/mobile/src/views/H5Marketing/index.vue:26(controlUrl直接作为:href)
影响
- 攻击者伪造 JWT 后在素材中心创建 article 类型素材并写入
<script>,员工/客户打开即执行。 - AI 导航返回的
recommendationsJSON 中若包含恶意脚本,会在 PC 管理后台执行。 - H5 营销页
controlUrl可设为javascript:alert(document.domain),用户点击触发。
修复
- 尽可能使用
{{ }}文本插值代替v-html。 - 必须渲染 HTML 时使用 DOMPurify 等库白名单过滤。
- 对 AI 返回的 JSON 字段做 HTML 编码后再拼接到 DOM。
13. Office/Excel 文档解析未显式禁用外部实体(中危,疑似)
位置
src/main/java/cn/iyque/service/impl/IYqueKnowledgeInfoServiceImpl.java:117-131src/main/java/cn/iyque/chain/loader/WordLoader.java:20-29src/main/java/cn/iyque/service/impl/IYqueCustomerSeasServiceImpl.java:57-89
代码
XWPFDocument document = new XWPFDocument(inputStream);
影响
若 transitive POI/EasyExcel 版本存在未修复的 XXE(历史上有多个 CVE),上传恶意 DOCX/XLSX 可触发 SSRF 或本地文件读取。当前未显式调用 setFeature("disallow-doctype-decl", true)。
修复
- 升级 EasyExcel/POI 到最新版。
- 对
WordLoader中DocumentBuilderFactory/XWPFDocument显式禁用 DTD/外部实体。 - 在沙箱/独立进程中解析不可信文档。
14. 生产环境 SQL 打印 + 堆栈/异常泄露(中危)
位置
src/main/resources/application.yml:56-59,79src/main/java/cn/iyque/controller/IYcallbackController.java:67src/main/java/cn/iyque/handler/GlobalExceptionHandler.java:10-18
影响
show-sql: true、format_sql: true、MyBatisStdOutImpl会把查询语句及参数打印到 stdout,生产日志可能包含客户手机号、会话内容。- 多处
e.printStackTrace()可能把异常堆栈写入日志。 GlobalExceptionHandler返回异常 message,可能泄露内部路径/SQL 错误。
修复
- 生产环境关闭 SQL 打印与
StdOutImpl。 - 使用统一日志框架,异常不返回给客户端。
- 日志脱敏。
15. 登录限流可被代理绕过且无验证码(中危)
位置
src/main/java/cn/iyque/aop/RateLimitAspect.java:20-61
代码
String ip = request.getRemoteAddr();
影响
部署在 CDN/反向代理后时,getRemoteAddr() 可能是代理 IP,导致所有用户共享计数;攻击者也可伪造 X-Forwarded-For 绕过。无验证码,仍可进行分布式暴力破解。
修复
- 优先读取并校验
X-Forwarded-For第一个非内网 IP,同时限制总请求速率。 - 登录加图形/短信验证码。
16. JS-SDK Ticket / OAuth URL 未校验 URL(低危)
位置
src/main/java/cn/iyque/controller/IYqueLoginController.java:101-122,131-142
影响
/weComRedirect返回的 OAuth URL 中redirectUrl来自请求参数;若前端直接跳转,存在开放重定向风险。/getAgentTicket对url无白名单,可为任意域名生成 JS-SDK 签名(受限于企微后台 JS 安全域名,实际利用需配合域名配置绕过)。
修复
- 对
redirectUrl做白名单校验。 /getAgentTicket仅接受已在配置中登记的业务域名。
第五阶段:审计总结
按严重性统计
| 严重 | 高危 | 中危 | 低危 | 不适用/未发现 |
|---|---|---|---|---|
| 5 | 6 | 3 | 1 | SQL 注入、命令注入、反序列化、SpEL/OGNL、LDAP/NoSQL 注入 |
Top 3 风险
- 认证完全失效:硬编码 JWT 密钥 + 无角色控制,任何人都能获得并滥用管理员权限。这是当前最严重的单点风险,建议立即修复。
- 未授权文件与 SSRF 入口:
/file/fileView/**路径遍历、/file/openUpload任意上传、/iYqueSys/uploadMediaIdSSRF 全部无需认证,可直接导致服务器沦陷或数据泄露。 - 企微密钥集中泄露:
/iYqueConfig/findIYqueConfig一次性返回所有企微 secrets,且数据库与配置文件中均明文存储,一旦任一环节失守即可控制企业微信应用。
需人工复核项
- 生产环境的 POI/EasyExcel 实际版本是否已修复已知 XXE CVE。
frontEnd/mobile/src/config.js中的硬编码 token 是否为开发调试残留,是否已流入生产构建。/file/fileView/**、/file/openUpload、/iYqueSys/uploadMediaId被加入 JWT 白名单是否为业务必需;如非必需建议移除白名单。- H5 营销页
controlUrl是否允许javascript:等伪协议。
后续深挖方向
- 在修复认证与授权后,对所有
saveOrUpdate接口做 IDOR/越权测试(当前几乎所有接口都通过id操作数据)。 - 对企业微信回调链路做完整加密签名绕过测试(需拿到 token/AES key)。
- 对 AI 功能做提示注入与输出安全测试,检查是否存在通过会话内容影响 RAG 知识库或触发 SSRF 的路径。
- 对前端所有
v-html点做动态 XSS 验证。
审计人建议:优先修复 JWT 密钥与访问控制,再关闭未授权文件/SSRF 入口,最后集中处理凭据存储与日志安全。以上发现均基于静态代码,实际利用效果可能因部署网络、WAF、企微后台配置而有所不同。