Snowy Admin 安全审计报告
- 审计对象:
snowy(SpringBoot 3.5.9 + Sa-Token + MyBatis-Plus + Redis + Vue3/AntdV 前端) - 审计目标:反序列化 / RCE / 命令执行 / SQL 注入 / 未授权访问 / 硬编码凭据 / XSS / SSRF / 越权(IDOR) 等可获取系统或用户权限/数据的漏洞
- 审计日期:2026-08-10
- 等级统计:3 CRITICAL · 9 HIGH · 17 MEDIUM · 7 LOW(含明确的安全/低风险排查结论)
每个「已验证」条目均由主流程逐行复核源码;标注「子代理上报」的条目来自 4 个并行审计子代理并经抽查核实。未发现可利用的反序列化/RCE 入口(见 §5)。
一、CRITICAL
C1. /mobile/** 全量接口未授权访问(匿名增删改移动端资源)
- 类型:未授权访问 / 越权 CRUD(Sa-Token 路由覆盖顺序缺陷)
- 位置:
snowy-web-app/.../core/config/GlobalConfigure.java:122("/mobile/**"在NO_LOGIN_PATH_ARR)、:240-241(/mobile/menu/**、/mobile/module/**在SUPER_PERMISSION_PATH_ARR)、:286-292(超管规则);snowy-plugin-mobile/.../resource/controller/MobileButtonController.java、MobileMenuController.java、MobileModuleController.java(全模块 0 个@SaCheckPermission/@SaCheckRole) - 利用链:Sa-Token 的
notMatch与match是两套独立集合,路径须「命中 SUPER 且 不命中 NO_LOGIN」才执行超管校验。/mobile/menu/**、/mobile/module/**均被/mobile/**(NO_LOGIN) 整体排除 → 超管校验永不执行;/mobile/button/**甚至不在 SUPER 列表。攻击者无需登录直接POST /mobile/button/add、/mobile/menu/edit、/mobile/module/delete、GET /mobile/menu/tree等任意增删改查移动端菜单/模块/按钮(含 C 端权限配置)。 - 修复:从
NO_LOGIN_PATH_ARR移除/mobile/**(仅放行真实静态资源如/mobile/static/**);/mobile/button/**补入SUPER_PERMISSION_PATH_ARR;三个 Controller 补方法级@SaCheckPermission。
C2. 短信/邮箱 6 位验证码可爆破 → 任意账号接管
- 类型:验证码爆破 / 认证绕过 / 账户接管
- 位置:
snowy-plugin-auth/.../login/service/impl/AuthServiceImpl.java:346-372(validValidCode失败不计数、不锁定、不删除)、:536-549(doLoginByPhone无失败锁定)、:208-247(getPhoneValidCode,前置图形验证码一次即可拿到reqNo)、:273-314(getEmailValidCode);白名单GlobalConfigure.java:126,128,136,138(/auth/b|/auth/c/getPhoneValidCode、doLoginByPhone等免登录) - 利用链:先解一次图形验证码拿到
phoneValidCodeReqNo,随后循环调用/auth/b/doLoginByPhone,6 位码(1e6 组合 / 默认 5 分钟有效期)无失败锁定地爆破任意已注册手机号,成功后为完整 B 端会话(再扫一遍即可接管所有账号);邮箱同理。攻击者也可对getPhoneValidCode不限频轰炸。 - 修复:校验失败增加计数并达阈值锁定手机号/验证码(强制重取);
doLoginByPhone/doLoginByEmail加独立失败锁定;发送接口按手机号/IP 限频。
C3. 文件上传路径穿越 → 任意文件写入
- 类型:路径穿越 / 任意文件写(潜在代码投递)
- 位置:
snowy-plugin-dev/.../file/service/impl/DevFileServiceImpl.java:348-366(genFileKey用FileUtil.getSuffix(originalFileName)取用户可控后缀拼路径)、:216-340(storageFile对普通上传无扩展名白名单);snowy-plugin-dev/.../file/util/DevFileLocalUtil.java:212-215(FileUtil.writeFromStream(inputStream, folder + "/" + bucketName + "/" + key)直接拼接写盘) - 利用链:任意登录 B 端用户上传文件(
/dev/file/upload*不在超管/免登录数组,仅 B 端登录校验),构造含../的文件名使getSuffix返回穿越片段,向服务器任意可写目录写入攻击者可控内容(可覆盖应用读取的配置、写入 cron 目录等)。注意 hutool 取最后一个点之后为后缀,最终写出文件多为无扩展名,故直接 webshell 受限,但任意写仍为 CRITICAL 类。 - 修复:文件名做白名单(仅
[A-Za-z0-9._-])、剥离所有路径分隔符与..;存储 key 改为纯服务端生成的fileId(不拼接用户文件名);写入目录规范化并校验startsWith根目录。
二、HIGH
H1. 用户权限转移可复制超管角色 → 提权
- 位置:
SysUserTransferServiceImpl.java:100-130(execute无数据范围校验)、SysUserRoleTransferProvider.java:93-124(COPY 模式appendRolesToUser无内置超管角色过滤)、BizTransferController.java:48-83(@SaCheckPermission("/biz/user/transfer/execute")) - 利用链:拥有
biz/user/transfer/*权限的 HR 类角色,先userSelector定位超管,再execute传{sourceUserId:超管, targetUserId:自己, transferMode:"COPY", transferAll:true, transferItems:[{resourceType:"ROLE"}]}→ 超管角色复制到自己账号,下次登录即超管。全程无getLoginUserDataScope()校验、无内置角色排除。 - 修复:
execute校验 source/target 数据范围;禁止 source 为内置超管或其角色含内置超管角色。 - 勘误:原上报「
userSelector用 Hutool JSON 泄露SysUser.password」不成立——SysUserServiceImpl.java:2130-2131,2060-2064显式.select(...)仅 8 个安全列,password 列为 null 未加载。
H2. /dev/file/authDownload 越权下载任意私有文件(IDOR)
- 位置:
DevFileServiceImpl.java:139-141(authDownload调unifiedDownload(..., true)直接跳过isDownloadAuth校验)、:144-168;/dev/file/**仅page|list|delete|detail在超管数组(GlobalConfigure.java:224-227),authDownload只需 B 端登录 - 利用链:
GET /dev/file/authDownload?id=<任意fileId>,任意登录用户可下载任何标记为「需授权下载」的私有文件(身份证、合同等)。 - 修复:
authDownload校验当前用户是否为该文件授权下载人,或移除该接口统一走带签名的download。
H3. 公有读桶上传 .html/.svg → 存储型 XSS
- 位置:
DevFileMinIoUtil.java:283(对象存储PUBLIC_READ桶)、DevFileController.java的uploadLocalReturnUrl/uploadDynamicReturnUrl等无扩展名校验 - 利用链:任意登录用户上传
.html/.svg到公有读桶得到可直接访问 URL,在站内域渲染实现钓鱼/存储型 XSS(同域可窃取 localStorage 中的 token)。 - 修复:上传扩展名白名单;公有读桶禁止
.html/.svg/.xml或强制Content-Disposition: attachment+X-Content-Type-Options: nosniff。
H4. C 端个人中心认证边界错配
- 位置:
GlobalConfigure.java:247-250(CLIENT_USER_PERMISSION_PATH_ARR仅/auth/c/**、/client/c/**)、:262-292;ClientUserCenterController.java;ClientUserServiceImpl.java:370-374等 - 利用链:
/client/userCenter/**未命中任何 C 端规则,落入 B 端StpUtil.checkLogin();但业务层用StpClientUtil.getLoginIdAsString()取 C 端会话。纯 C 端用户功能失效;B 端员工若同时持 C 端 token 即可调用本应仅 C 端使用的接口,B/C 会话隔离失效。 - 修复:将
/client/userCenter/**纳入CLIENT_USER_PERMISSION_PATH_ARR,统一StpClientUtil.checkLogin()。
H5. 潜伏 SQL 注入(字符串拼接进 wrapper.apply())
- 位置:
SysUserApiProvider.java:96-116(getUserIdListByOrgIdList/getUserIdListByPositionIdList) - 利用链:
orgIdList直接拼进POSITION_JSON LIKE '%'+id+'%'再.apply(orConditionSql)。已排查全仓库:两方法仅在SysUserApi接口声明,无任何 HTTP Controller 直接调用,属潜伏高危——一旦未来模块把外部可控 id 列表传入即触发(可至 RCE 级)。 - 修复:参数化
like+ 多条or条件。
H6. 第三方 Token 直接登录 B 端(免登录)
- 位置:
AuthServiceImpl.java:1211-1228,1235-1295、GlobalConfigure.java:143(/auth/b/doLoginByThirdToken在免登录白名单) - 利用链:完全信任第三方 userinfo 接口返回的
account/email/phone,按「account→email→phone」匹配本地用户后execLoginB直接登录。若第三方账号同名或 access_token 校验不严,可同名接管本地账号(含超管)。第三方地址为配置项默认关闭,一旦开启即高风险信任链。 - 修复:校验第三方身份与本地用户强绑定关系(不可仅凭同名);校验 token 过期/范围;登录前校验用户启用状态。
H7. OIDC/OAuth 登录 CSRF(state 校验降级忽略)
- 位置:
AuthOidcClient.java:148-155、AuthOauthClient.java:100-112("Illegal state" 时ignoreCheckState=true重试) - 利用链:SSO 启用时,攻击者用自己获取的
code+ 任意/无效state诱导受害者访问回调,服务端忽略 state 校验把受害者浏览器登录为攻击者三方身份(登录 CSRF / 账号绑定混乱)。 - 修复:强制校验 state 与会话一致,缺失/不匹配即拒绝。
H8. SM2 私钥 / SM4 密钥硬编码(可逆混淆)
- 位置:
snowy-common/.../util/CommonCryptogramUtil.java:40(公钥)、:43-61(SM2 私钥 DATA/MASK)、:64-76(SM4 密钥 DATA/MASK)、:79-91(SM4 IV)、:94-96(运行时 XOR 还原);CommonKeyUtil.reconstruct即逐字节 XOR - 利用链:混淆可逆,源码/JAR 即可还原 SM2 私钥(可解密登录密码 SM2 密文)与 SM4 密钥/IV(敏感列加解密)。即「密码学保护」在分发物面前完全失效,非 HTTPS 部署下登录口令可被截获还原。
- 修复:私钥移出源码(部署时环境变量/KMS 注入),至少每次发布轮换。
H9. Druid 监控台 / knife4j / 数据库默认弱口令且暴露
- 位置:
snowy-web-app/src/main/resources/application.properties:65-67(druidadmin/123456)、:120(/druid/**免登录)、:138-142(knife4jadmin/123456)、:117-119(/doc.html、/v3/api-docs/**、/webjars/**免登录)、:30-32(DBroot/12345678);DruidConfigure.java仅去广告,未加额外鉴权 - 利用链:
/druid/**免登录 + 默认口令 → 查看 SQL/会话/URI 统计(信息泄露,会话若含 token 更糟);knife4j 泄露全部 API 文档;DB 默认口令便于内网横向。 - 修复:生产关闭 druid stat-view 或强口令并移出免登录白名单;knife4j 生产关闭或强口令;DB 口令环境变量注入强随机。
三、MEDIUM
| # | 发现 | 位置 | 说明 |
|---|---|---|---|
| M1 | /dev/weakPassword/** 缺失超管隔离 |
DevWeakPasswordController.java:58-117 |
任何登录 B 端用户可增删改弱口令字典(与 /dev/monitor、/dev/log 需超管不一致)。原报「可探测他人口令」过强,实为安全策略数据可被篡改 + 信息泄露,定级 MEDIUM |
| M2 | CORS 全开放 | GlobalConfigure.java:309,315 Origin:*+Allow-Headers:* |
任意站点可对已持有 token 的浏览器发起请求/读响应 |
| M3 | 防重复/限频依赖可伪造 XFF | CommonIpAddressUtil.java:83、GlobalConfigure.java:357-417(CommonNoRepeatAop) |
伪造 X-Forwarded-For 绕过防重复提交与 IP 限频 |
| M4 | WebSocket 无 Origin 校验、token 走 query | DevMessageWebSocket.java:71-103 |
跨站 WebSocket 劫持读站内消息;token 经 URL/日志泄露 |
| M5 | 站内消息详情越权 | SysIndexServiceImpl.java:118-120、SysUserServiceImpl.java:2163-2165 → devMessageApi.detail(id) |
任意登录用户读任意消息 id 内容+接收人列表 |
| M6 | 删除他人日程 IDOR | SysIndexServiceImpl.java:90-96 |
removeByIds 无归属校验(scheduleList 有、delete 无) |
| M7 | biz 用户 detail 无数据范围 | BizUserServiceImpl.java:336-339 |
disable/enable/resetPassword/grantRole 都有校验,唯独 detail 无 → 跨机构看任意用户资料 |
| M8 | biz 多接口缺 @SaCheckPermission |
BizUserController.java:308-309、BizOrgController.java:186-187、BizPositionController.java:161-162、BizDictController.java:82-83(treeAll)、BizIndexController.java:55-91 |
任意登录用户可拿机构/岗位祖先链、全部字典、公告 |
| M9 | 公告草稿/停用可见 + content 未过滤 | BizIndexServiceImpl.java:58-62,76-79、BizNoticeServiceImpl.page:50 |
首页接口看草稿/停用公告;BizNotice.content 原始 HTML,有编辑权限者可投递存储型 XSS |
| M10 | 账号枚举 | AuthServiceImpl.java:510-529(ACCOUNT_ERROR vs PWD_ERROR)、ClientUserServiceImpl.java:495-497,544-546 |
登录/找回密码差异化报错暴露账号存在性 |
| M11 | 绑定邮箱条件反转(逻辑缺陷) | ClientUserServiceImpl.java:873-875,955-957 |
if (isEmpty(userByEmail(email))) throw "邮箱已存在"——新邮箱被拒、他人占用邮箱反而可绑定,可致邮箱登录身份冲突/接管 |
| M12 | 短信/邮件发送无频率限制 | AuthServiceImpl.java:208-247,273-314、/auth/c/getPhoneValidCode、/client/userCenter/findPasswordGetPhoneValidCode |
轰炸 + 资费耗尽 + 验证码爆破前置 |
| M13 | 服务器运行信息泄露 | SysIndexServiceImpl.java:166-168 → devApi.getSysRunInfo() |
/sys/index/sysInfo 未入超管数组,任意登录用户可获 javaVersion/osName/serverIp |
| M14 | CommonSqlUtil.scopeIn 仅过滤单引号 |
CommonSqlUtil.java:82-89 |
inSql 只 replace("'",""),其余 SQL 元字符未处理;当前调用方为登录态 ID,属加固点 |
| M15 | 验证码「先删后比」并发复用 | ClientUserServiceImpl.java:450-472、SysUserServiceImpl.java:595 |
两个并发请求可读到同一验证码并都通过 |
| M16 | 前端存储型 XSS(v-html,token 窃取链) | snowy-admin-web/src/components/HomeCard/BizNoticeCard/detail.vue:35、src/views/biz/notice/detail.vue:35、src/views/dev/config/emailTemplateConfig/preview.vue:3 |
v-html="formData.content" 未净化;token 存 localStorage,XSS=完全接管(配合 M9/H3 存储源) |
| M17 | fastjson 传递依赖(三方协议解析) | snowy-plugin-auth/.../protocol/{oauth,oidc,cas,saml,jwt}/... |
第三方响应解析用 fastjson,autoType 默认关闭,需监控 CVE;第三方响应若攻击者可控需关注 |
四、LOW
| # | 发现 | 位置 | 说明 |
|---|---|---|---|
| L1 | TraceId 头注入日志 | CommonTraceIdUtil.java:32-38 |
traceId 头原样入日志,可伪造日志行(审计污染) |
| L2 | 对象存储下载 SSRF(潜伏) | DevFileServiceImpl.java:167(HttpUtil.downloadBytes(storagePath)) |
路径由服务端拼接 fileId,需先攻破存储配置才可指内网 |
| L3 | 下载签名密钥硬编码 + 签名无时效 | DevFileServiceImpl.java:78(FILE_DOWNLOAD_SIGN_SECRET="snowy_file_download_secret_key_2025")、:419-445 |
密钥泄露可伪造任意文件下载签名,且永久有效 |
| L4 | OTP 登录无失败锁定 | AuthServiceImpl.java:1113-1186(doLoginByOtp) |
TOTP 6 位码 30s 窗口内可爆破 |
| L5 | 内置 RSA 私钥死代码 | AuthCertUtil.java:48,50,108-117 |
硬编码 RSA 私钥 + 证书,全仓无调用方,建议删除 |
| L6 | Redis 默认无密码 | application.properties:98 |
spring.data.redis.password= 空,内网可读写会话/缓存 |
| L7 | 数据库默认口令 | application.properties:30-32、application-docker.yml:12 |
root/12345678 生产未改即内网横向 |
五、明确安全 / 低风险排查结论(对应你关注的漏洞类)
- 反序列化/RCE:Jackson 未开启
enableDefaultTyping/多态反序列化 → 无 gadget 风险;Redisson 用JsonJacksonCodec、RedisTemplate 用RedisSerializer.json()→ 非 JDK 反序列化,无 RCE。fastjson 仅在 auth 三方协议类中做响应解析,autoType 默认关闭(见 M17)。 - 命令执行:
CommonNetWorkInfoUtil.java:49-60的Runtime.exec为固定命令(netstat -e/ifconfig),不可注入,已排除。 - SQL 注入(mapper XML):43 个 mapper XML 全部为空壳(DOCTYPE + 空
<mapper>),无${}/foreach拼接 → 无注入面。唯一拼接点在SysUserApiProvider(H5,潜伏)。 - 异常信息泄露:
GlobalErrorAttributesHandler/GlobalExceptionHandler对非CommonException统一返回「服务器异常」,不泄露堆栈。 - 响应头注入:
CommonDownloadUtil.java:53的Content-Disposition用URLUtil.encode(fileName)百分号编码,阻断 CRLF 注入。 - gen 模块路径穿越(
GenBasicServiceImpl.java:603-781):表名/包名/类名可影响生成文件路径与内容,但/gen/basic/**、/gen/config/**均在SUPER_PERMISSION_PATH_ARR(GlobalConfigure.java:238-239),仅超管可达——属防御纵深缺陷(超管被攻破后可投递带后门代码),定级 MEDIUM,未计入上表。
六、修复优先级路线图
P0(立即)
- C1 修 Sa-Token 路由覆盖顺序 + mobile 控制器补注解
- C2 验证码失败锁定 + 登录接口限频
- C3 文件名白名单/服务端生成 key,阻断任意文件写
P1(尽快)
4. H1 transfer 加数据范围 + 内置角色保护
5. H2 authDownload 授权校验
6. H4 C 端个人中心认证规则纠正
7. H9 生产环境默认凭据全部更换 + /druid/** 移出免登录
P2(本迭代)
8. H3/H5/H6/H7/H8 + 全部 MEDIUM(重点 M2 CORS、M4 WebSocket、M16 前端 XSS)
P3(持续)
9. 全部 LOW + 密钥轮换机制 + 依赖 CVE 监控(fastjson、hutool)
本报告基于源码静态审计,未做运行时验证。所有 file:line 以当前 master(2c9da36e)为准。
正文完