iYqueCode-安全审计报告-20260828

35次阅读
没有评论

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. 关键数据流示例

  1. 登录:IYqueLoginController.login → 与配置中的明文密码比对 → JwtUtils.generateToken → 返回 JWT。
  2. 文件上传/预览:FileController.upload/openUpload → FileUtils 扩展名校验 → 写入 ./upload → 返回 fileViewUrl;FileController.readFile 直接拼接 filename 返回文件。
  3. 群发: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 系统而言,最有价值的目标是:

  1. 企业微信配置密钥(corpId、agentSecret、token、encodingAESKey、会话存档私钥)。
  2. 管理员 JWT / 管理员账号。
  3. 系统上的客户数据、会话存档。
  4. 服务器内网访问能力(SSRF / 任意文件读取)。

攻击面优先级 Top 5

  1. 认证绕过:硬编码 JWT 密钥 + 前端硬编码 token → 可直接获得后台全部权限。
  2. 未授权文件相关接口:/file/fileView/** 路径遍历、/file/openUpload 任意文件上传。
  3. 未授权 SSRF:/iYqueSys/uploadMediaId 让服务端拉取任意 URL。
  4. 授权缺失导致的越权/敏感信息泄露:/iYqueConfig/findIYqueConfig 返回全部企微密钥;所有管理接口无角色隔离。
  5. 服务端本地文件读取/SSRF:FileUtils.downloadImage 非 HTTP(S) 分支拼接本地路径,多处业务入口调用。

审计计划

按优先级依次深挖:

  1. 认证/授权与 JWT 实现。
  2. 文件上传、下载、预览链路。
  3. SSRF / URL 拉取链路。
  4. 配置与凭据泄露。
  5. 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:13
  • frontEnd/mobile/src/config.js:11-12
  • src/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-35
  • src/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-209
  • src/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-159
  • src/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-180
  • src/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-28
  • src/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-48
  • src/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:16
  • frontEnd/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 导航返回的 recommendations JSON 中若包含恶意脚本,会在 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-131
  • src/main/java/cn/iyque/chain/loader/WordLoader.java:20-29
  • src/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,79
  • src/main/java/cn/iyque/controller/IYcallbackController.java:67
  • src/main/java/cn/iyque/handler/GlobalExceptionHandler.java:10-18

影响

  • show-sql: true、format_sql: true、MyBatis StdOutImpl 会把查询语句及参数打印到 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 风险

  1. 认证完全失效:硬编码 JWT 密钥 + 无角色控制,任何人都能获得并滥用管理员权限。这是当前最严重的单点风险,建议立即修复。
  2. 未授权文件与 SSRF 入口:/file/fileView/** 路径遍历、/file/openUpload 任意上传、/iYqueSys/uploadMediaId SSRF 全部无需认证,可直接导致服务器沦陷或数据泄露。
  3. 企微密钥集中泄露:/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、企微后台配置而有所不同。

正文完
 0