SnowyAdmin安全审计报告

21次阅读
没有评论

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(druid admin/123456)、:120(/druid/** 免登录)、:138-142(knife4j admin/123456)、:117-119(/doc.html、/v3/api-docs/**、/webjars/** 免登录)、:30-32(DB root/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 生产未改即内网横向

五、明确安全 / 低风险排查结论(对应你关注的漏洞类)

  1. 反序列化/RCE:Jackson 未开启 enableDefaultTyping/多态反序列化 → 无 gadget 风险;Redisson 用 JsonJacksonCodec、RedisTemplate 用 RedisSerializer.json() → 非 JDK 反序列化,无 RCE。fastjson 仅在 auth 三方协议类中做响应解析,autoType 默认关闭(见 M17)。
  2. 命令执行:CommonNetWorkInfoUtil.java:49-60 的 Runtime.exec 为固定命令(netstat -e/ifconfig),不可注入,已排除。
  3. SQL 注入(mapper XML):43 个 mapper XML 全部为空壳(DOCTYPE + 空 <mapper>),无 ${}/foreach 拼接 → 无注入面。唯一拼接点在 SysUserApiProvider(H5,潜伏)。
  4. 异常信息泄露:GlobalErrorAttributesHandler / GlobalExceptionHandler 对非 CommonException 统一返回「服务器异常」,不泄露堆栈。
  5. 响应头注入:CommonDownloadUtil.java:53 的 Content-Disposition 用 URLUtil.encode(fileName) 百分号编码,阻断 CRLF 注入。
  6. gen 模块路径穿越(GenBasicServiceImpl.java:603-781):表名/包名/类名可影响生成文件路径与内容,但 /gen/basic/**、/gen/config/** 均在 SUPER_PERMISSION_PATH_ARR(GlobalConfigure.java:238-239),仅超管可达——属防御纵深缺陷(超管被攻破后可投递带后门代码),定级 MEDIUM,未计入上表。

六、修复优先级路线图

P0(立即)

  1. C1 修 Sa-Token 路由覆盖顺序 + mobile 控制器补注解
  2. C2 验证码失败锁定 + 登录接口限频
  3. 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)为准。

正文完
 0
评论(没有评论)