管伊佳ERP(jshERP)安全审计报告
审计时间: 2026-08-11
审计对象:jshERP-boot(SpringBoot 2.0.0 / Java 8 / MyBatis-Plus 3.0.7.1)+jshERP-web(Vue 2.7 / Ant Design Vue)
审计范围: 反序列化、命令执行/RCE、SQL 注入、未授权访问、硬编码凭据、XSS、SSRF、越权/IDOR 等可获取系统/用户/应用权限与数据的问题
审计方式: 源码级只读审计 + 5 个并行深度子代理 + 关键链路手工核验。未修改任何文件。
1. 总体结论
该 ERP 系统存在 1 条可直接打穿的完整 RCE 链、多处严重越权/提权、大量未授权接口与敏感信息泄露,以及若干高危放大器。综合评级:高危。
核心根因:整个后端只有 LogCostFilter 这一个认证过滤器,且它只校验"是否登录"、从不校验"角色/权限/归属";而系统又默认开放自助注册、默认 admin/123456、默认关闭验证码。三条路叠加,任意匿名访问者可轻松获得高权限乃至服务器代码执行。
最严重的攻击链(端到端)
匿名访问者
│ ① 默认开放注册 /user/registerUser (后端不校验 register_flag)
▼
任意普通用户
│ ② /user/update 传入 {"id":1,"password":"<攻击者md5>"} → 越权重置超管密码
│ (updateByPrimaryKeySelective, 仅校验"已登录", 不校验归属)
▼
超管 admin 登录
│ ③ /plugin/uploadInstallPluginJar 上传构造的 pf4j 插件 jar
│ (starblues springboot-plugin-framework 2.2.1, runMode=prod, 无类型/签名校验)
▼
服务器任意代码执行 (RCE)
即使不考虑注册开放,默认凭据 admin / 123456 + 验证码默认关闭(checkcode_flag 在初始化 SQL 中未插入)也可直接完成 ①。
2. 漏洞汇总表(按严重程度排序)
| # | 严重程度 | 类型 | 位置 | 一句话结论 |
|---|---|---|---|---|
| 1 | 🔴 严重 | 任意代码执行 RCE | controller/PluginController.java:243-263 |
admin 上传 pf4j 插件 jar → uploadPluginAndStart 动态加载执行,任意代码运行在 JVM |
| 2 | 🔴 严重 | 越权/垂直提权 | service/UserService.java:176-192 |
任意已登录用户可 updateByPrimaryKeySelective 重置**任意用户(含超管)**密码 |
| 3 | 🔴 严重 | 认证体系失效(根因) | filter/LogCostFilter.java:47 |
全站仅校验"是否登录",无任何角色/权限/归属校验;且默认开放注册、默认 admin/123456 |
| 4 | 🟠 高 | 越权/水平提权 | service/UserBusinessService.java(updateUserBusiness) |
可自行把角色绑定为管理员,一步提权 |
| 5 | 🟠 高 | 未授权访问 | config/Swagger2Config.java + LogCostFilter.java:16 |
/doc.html、/v2/api-docs、/webjars 匿名暴露全部接口文档 |
| 6 | 🟠 高 | 越权/IDOR | 全库 getXxx(id)/update/delete 接口 |
按 id 直操作,无租户/归属过滤,跨租户任意读改删 |
| 7 | 🟠 高 | 任意文件删除 | service/SystemConfigService.java:350-381 |
单据 fileName 字段可含 ../,删除单据时 Files.delete 任意文件 |
| 8 | 🟠 高 | 任意路径安装插件/写配置 | controller/PluginController.java:217-235, 270-288 |
admin 可 installByPath 加载服务器任意 jar、uploadConfigFile 任意文件写入 |
| 9 | 🟠 高 | 敏感信息泄露 | controller/UserController.java(/user/info?id=) |
返回任意用户对象,含 MD5 口令散列 |
| 10 | 🟡 中 | 未授权文件读取 | controller/SystemConfigController.java:228-286 + LogCostFilter.java:17 |
/systemConfig/static/** 免登录读取上传根目录任意文件(跨租户) |
| 11 | 🟡 中 | 存储型 XSS | service/MsgService.java + jshERP-web/.../ShowAnnouncement.vue:19 |
公告 msgContent 原样存储,前端 v-html 渲染 |
| 12 | 🟡 中 | 前端代码注入(eval) | jshERP-web/src/utils/util.js:113 |
菜单 URL 模板用 eval() 渲染 {{...}} 占位符 |
| 13 | 🟡 中 | SSRF(配置驱动/盲) | service/UserService.java:958-988、utils/HttpClient.java |
weixinUrl 等平台配置指向内网时,/user/weixinLogin 免登录触发内网/云元数据请求 |
| 14 | 🟡 中 | 硬编码凭据 | application.yml:17-24 |
DB root/123456、Redis 1234abcd;默认账号 admin/jsh/test123 均为 MD5("123456") |
| 15 | 🟡 中 | 弱口令存储 | utils/Tools.java:582-588 + Login.vue:206 |
无盐 MD5,且口令在前端 MD5 后传输 |
| 16 | 🟡 中 | 租户隔离失效 | config/TenantConfig.java + utils/Tools.java:619 |
tenant_id 由 token 后缀解析,无后缀 → 0 → 超级管理员无任何租户过滤 |
| 17 | 🟡 中 | SQL 注入放大器 | application.yml:17 |
allowMultiQueries=true 允许堆叠查询(当前无已利用注入点,加固项) |
| 18 | 🟢 低 | fastjson 反序列化 sink | utils/ExtJsonUtils.java:99 |
fastjson 1.2.83 默认 autoType 关闭暂不可利用;响应序列化解析用户可控 ext 字段,属理论风险 |
| 19 | 🟢 低 | 开放重定向 | jshERP-web/src/permission.js:52-53 |
登录后按 from.query.redirect 跳转,可跳任意站 |
| 20 | 🟢 低 | 过期依赖 | pom.xml:116-120 |
commons-io 1.3.2(2011 年版本),无直接可利用调用点 |
3. 根因分析:认证与授权体系
3.1 只校验登录,不校验权限
jshERP-boot/src/main/java/com/jsh/erp/filter/LogCostFilter.java
@WebFilter(filterName = "LogCostFilter", urlPatterns = {"/*"}, ...) // 覆盖所有路径
public class LogCostFilter implements Filter {
public void doFilter(...) {
Object userId = redisService.getObjectFromSessionByKey(servletRequest,"userId"); // 仅查"是否登录"
if(userId!=null) { //如果已登录,不阻止
chain.doFilter(request, response);
return;
}
// ... 白名单放行匿名请求
servletResponse.setStatus(500);
servletResponse.getWriter().write("loginOut"); // 未登录则拒绝
}
}
- 只校验
X-Access-Token→ Redis 中是否有userId,不校验角色、不校验资源归属。 - 因此:任何已登录用户(哪怕刚注册的普通租户用户)可调用全部接口,包括管理接口、跨租户数据接口。
- 唯一特权判断散布在各 Controller 里,形式为
BusinessConstants.DEFAULT_MANAGER.equals(userInfo.getLoginName())(即loginName=="admin"字符串比对),且多数写接口根本没有这个判断。
3.2 认证体系默认关闭了三道闸门
| 闸门 | 现状 | 证据 |
|---|---|---|
| 注册开关 | 后端 registerUser 不校验 register_flag,前端才做判断 |
jshERP-web/src/views/user/Login.vue:349-350;service/UserService.java:612 |
| 登录验证码 | 仅当平台配置 checkcode_flag=="1" 才启用,初始化 SQL 未插入该配置 → 默认关闭 |
service/UserService.java validateCaptcha;docs/jsh_erp.sql |
| 默认口令 | 超管 admin / 普通账号 jsh、test123 口令均为 e10adc... = MD5("123456") |
docs/jsh_erp.sql:997-999;constants/BusinessConstants.java:160,208 |
3.3 匿名白名单过大
LogCostFilter.java:16-18,52-55 放行的匿名路径包括:
/jshERP-boot/platformConfig/getPlatform
/jshERP-boot/v2/api-docs
/jshERP-boot/webjars
/jshERP-boot/systemConfig/static
/jshERP-boot/api/plugin/wechat/weChat/share
/jshERP-boot/api/plugin/general-ledger/pdf/voucher
/jshERP-boot/api/plugin/tenant-statistics/tenantClean
/jshERP-boot/user/login | register | weixinLogin | weixinBind | registerUser | randomImage
/jshERP-boot/doc.html
其中 registerUser、weixinLogin、systemConfig/static、swagger 文档均造成下文的具体漏洞。
4. 严重漏洞详情
4.1 [严重] 插件 JAR 上传 → 动态加载 → RCE
位置: controller/PluginController.java:243-263
依赖: springboot-plugin-framework 2.2.1(starblues,pom.xml:27-30)+ application.yml:47 plugin.runMode: prod
@PostMapping("/uploadInstallPluginJar")
public BaseResponseInfo install(MultipartFile file, HttpServletRequest request, HttpServletResponse response){
User userInfo = userService.getCurrentUser();
if(BusinessConstants.DEFAULT_MANAGER.equals(userInfo.getLoginName())) { // 仅"admin"字符串比对
pluginOperator.uploadPluginAndStart(file); // 上传并立即加载/启动插件
res.code = 200; res.data = "导入成功";
}
}
利用链:
- 获得 admin 登录态(默认
admin/123456、验证码默认关闭,或经 §4.2 越权重置超管密码)。 POST /jshERP-boot/plugin/uploadInstallPluginJarmultipart 上传一个构造的 pf4j 插件 jar。- starblues 在
runMode=prod下执行uploadPluginAndStart:把 jar 安装进插件目录并立即创建插件 Spring 上下文、注册其 Controller、调用插件Bootstrap.start()。 - 插件 jar 中任意代码在 JVM 内执行;JAR 内容与文件名均无类型/白名单/签名校验。
危害: 服务器任意代码执行/命令执行(等价 RCE)。若进程以 root 运行则完全控制主机;插件同时暴露 /api/plugin/** 路由,可持续驻留。
修复建议:
- 移除或默认关闭插件动态加载能力(
runMode: dev,或去掉上传安装接口); - 插件必须做签名校验 / 受信来源限制 / 二次认证,不依赖单一
loginName=="admin"字符串判断; - 对
uploadInstallPluginJar、installByPath、uploadPluginConfigFile增加独立的、强认证的管理通道。
4.2 [严重] 越权重置任意用户密码 → 直达 RCE
位置: service/UserService.java:176-192(updateUser)
public int updateUser(JSONObject obj, HttpServletRequest request) throws Exception{
User user = JSONObject.parseObject(obj.toJSONString(), User.class); // 含 id/password,全由客户端提供
...
Object userId = redisService.getObjectFromSessionByKey(request,"userId");
if (userId != null) { // 只校验"已登录",不校验操作对象是否属于自己
result = userMapper.updateByPrimaryKeySelective(user); // 按客户端 id 更新任意用户
}
配套问题:
resetPwd(UserService.java:217)只禁止"admin".equals(loginName),但没有拦updateUser这条更宽的路径;controller/UserController.java的/user/update、/user/resetPwd均接受id+password(MD5);updateByPrimaryKeySelective无租户/归属过滤,跨租户可改;loginName也可被改写,普通用户可把自己的 loginName 改成admin。
利用链: 任一登录用户 → PUT /jshERP-boot/user/update 发送 {"id":1,"password":"<攻击者md5>"} → 超管密码被重置 → 登录 admin → §4.1 插件 RCE。
修复建议: 写接口一律校验"只能操作自己 / 租户内且角色允许"的对象;id、password、loginName 等敏感字段不允许由请求体直接映射;updateUser 与 resetPwd 统一防护。
4.3 [高] 自行绑定管理员角色(水平提权)
位置: service/UserBusinessService.java(updateUserBusiness)
UserBusiness 表保存 userId → 角色/机构/仓库/权限 绑定。updateUserBusiness 无任何权限校验,updateByPrimaryKeySelective 按请求体更新。普通用户可把 keyId(角色 id)改为管理员角色(如 roleId=10 或角色 id=1),自行提权为管理员,绕过 §4.2。
修复建议: 角色绑定只能由超管操作;对 keyId、type(role/user/org 等)做服务端归属校验。
4.4 [高] 任意 IDOR(越权访问/修改/删除数据)
位置: 全库 Controller + Service。几乎所有 getXxx(id)、updateXxx、deleteXxx、batchDeleteXxx 接口都直接以请求中的 id/ids 查询或写库,没有任何"数据属于当前用户/租户"的过滤。
- 典型:
/depotHead/getDetail?id=、/user/info?id=、/material/list、各batchDeleteXxxByIds。 - 配合 §3.1,低权限/跨租户账号可读取、修改、删除任意租户的单据、商品、客户、用户等数据。
修复建议: 建立统一的数据归属校验(当前登录用户/租户 vs 数据 owner),在 Service 层强制过滤;ids 批量操作同样校验。
4.5 [高] 未授权访问:Swagger 全量接口文档
位置: config/Swagger2Config.java + LogCostFilter.java:16
@EnableSwagger2
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any()) // 所有接口
.paths(PathSelectors.any()) // 所有路径
/jshERP-boot/v2/api-docs、/jshERP-boot/doc.html、/jshERP-boot/webjars/**均在匿名白名单中。- 攻击者可离线拿到全部接口的参数、实体结构、示例请求,大幅降低其余漏洞(越权、注入、插件上传等)的利用成本。
修复建议: 生产环境关闭 swagger(profile 隔离);至少把 swagger 路径从匿名白名单移除并要求登录+管理员。
4.6 [高] 任意文件删除(路径穿越)
位置: service/SystemConfigService.java:350-381(deleteFileByPathList)
for (String path : pathArr) {
...
Path sourcePath = Paths.get(filePath + File.separator + path); // path 未做任何校验
Path targetPath = Paths.get(filePath + File.separator + DELETED + File.separator + path);
...
Files.copy(sourcePath, targetPath, ...);
Files.delete(sourcePath); // 删除任意文件
}
利用链: 单据/商品记录的 fileName 字段由请求体直接映射(如 DepotHeadService.addDepotHeadAndDetail 的 JSONObject.parseObject(beanJson, DepotHead.class),无过滤)。认证用户创建单据时把 fileName 设为 ../../../../var/www/.../config.yml 之类的路径,再调用删除单据接口(batchDeleteDepotHeadByIds → deleteFileByPathList),Files.delete 即删除目标文件。getDirByPath 本身允许 /,故斜杠不受限、.. 不被清理。
危害: 任意文件删除 → 删除配置、数据文件、.jar、应用自身文件,造成 DoS/业务中断。
修复建议: 删除前对路径做规范化(toRealPath)并强制校验位于上传根目录内;fileName 入参时即过滤 ..。
4.7 [高] 敏感信息泄露:任意用户口令散列
位置: controller/UserController.java(/user/info?id=)
User user = userService.getUser(id); // 返回完整 User,含 password 字段(MD5)
/user/info?id=对任意 id 返回用户对象,password字段未被置空。- 拿到超管口令散列后可离线破解(MD5 无盐,见 §4.14)或直接复用。
修复建议: 查询出参统一 setPassword(null)(或 DTO 过滤);同样检查其他返回 User 的接口。
5. 中危漏洞详情
5.1 未授权文件读取(跨租户)
位置: controller/SystemConfigController.java:228-286(/systemConfig/static/**)+ LogCostFilter.java:17 白名单
@GetMapping(value = "/static/**")
public void view(...) {
String imgPath = extractPathFromPattern(request);
imgPath = imgPath.replace("..", ""); // 弱过滤
fileUrl = systemConfigService.getFileUrlLocal(imgPath); // filePath + "/" + imgPath
inputStream = new BufferedInputStream(new FileInputStream(fileUrl));
- 免登录即可读取上传根目录
/opt/jshERP/upload下任意文件(其他租户上传的附件、导入的 Excel、导出的单据等)。 LogCostFilter.java:41拦截..、%2e+replace("..","")双重抑制,限制读取范围在上传根目录内,无法逃逸到/etc/passwd(信息泄露面可控但真实存在)。- 若
fileUploadType=2(OSS),则按aliOss_linkUrl拼接 URL 发起请求,存在配置驱动 SSRF(§5.4)。
修复建议: 从匿名白名单移除,改为登录+租户校验;或按租户子目录隔离;用白名单扩展名+规范化路径。
5.2 存储型 XSS
位置: 后端 service/MsgService.java(insertMsg)+ 前端 jshERP-web/src/components/tools/ShowAnnouncement.vue:19
<!-- 前端:公告展示 -->
<span v-html="record.msgContent"></span>
MsgService.insertMsg仅排除loginName=="admin",对任意普通用户开放,msgContent原样入库(只判断"不是 admin 才允许发公告")。- 前端用
v-html直接渲染 → 普通用户可发公告,<img src=x onerror=...>/<script>在所有看公告的用户浏览器里执行,可窃取管理员会话、构造钓鱼界面。
修复建议: 后端对 msgContent 做 HTML 编码/白名单富文本清洗;前端改为文本渲染或使用 DOMPurify 过滤。
5.3 前端菜单 URL eval 代码注入
位置: jshERP-web/src/utils/util.js:113
let URL = (item.url||'').replace(/{{([^}}]+)?}}/g, (s1, s2) => eval(s2));
- 菜单
url字段来自后端菜单表(数据库),模板占位符{{...}}被eval()直接执行。 - 结合 §4.4/§5.2,任何能改菜单数据或诱导管理员访问含恶意占位符菜单的路径,都能在登录用户浏览器执行任意 JS。
- 即使目前无直接入口,
eval本身也是明确的高危反模式,应移除。
修复建议: 用静态映射/replace 字典替代 eval;菜单 url 在前端白名单化。
5.4 SSRF(配置驱动,未授权触发)
位置: service/UserService.java:958-988(getUserByWeixinCode)、service/PlatformConfigService.java:199-221、utils/HttpClient.java:32-55
String weixinLogin = platformConfigService.getPlatformConfigByKey("weixinUrl").getPlatformValue() + WEIXIN_LOGIN;
String url = weixinLogin + "?appid=" + weixinAppid + "&secret=" + weixinSecret + "&js_code=" + weixinCode + "&grant_type=authorization_code";
JSONObject jsonObject = HttpClient.httpGet(url); // 服务端发起请求
weixinUrl/aliOss_linkUrl等存于jsh_platform_config表,写入需 admin,但触发免登录:/user/weixinLogin、/user/weixinBind在白名单。- 一旦平台配置被(恶意)admin 指向内网地址,任意未授权请求即可让服务器对
http://127.0.0.1:*、http://169.254.169.254/*(云元数据)或内网任意端口发起 GET;响应仅提取openid/access_token返回,故主要为盲 SSRF。
修复建议: 服务端出站请求统一经 SSRF 防护(禁内网/链路本地地址、限协议与端口);weixinUrl 校验 https 域名;敏感平台配置不返回给低权限用户。
5.5 硬编码凭据 / 默认口令
位置: application.yml:17-24、docs/jsh_erp.sql:997-999、constants/BusinessConstants.java:160,208、service/UserService.java:157-161
# application.yml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/jsh_erp?...&allowMultiQueries=true&...&useSSL=false
username: root
password: 123456
redis:
password: 1234abcd
-- docs/jsh_erp.sql
-- 内置账号: admin / jsh / test123
-- password = 'e10adc3949ba59abbe56e057f20f883e' = MD5('123456')
// service/UserService.java:157-161 —— 新增用户默认口令
String password = "123456";
password = Tools.md5Encryp(password);
- 数据库
root/123456、Redis1234abcd硬编码在配置仓库中;useSSL=false。 - 所有内置账号口令均为 MD5("123456")。
修复建议: 凭据改由环境变量/密钥管理(如 Vault/KMS)注入;强制修改默认口令;开启验证码与登录限流。
5.6 弱口令存储:无盐 MD5 + 前端 MD5 传输
位置: utils/Tools.java:582-588(md5Encryp)、jshERP-web/src/views/user/Login.vue:206
// 无盐 MD5
MessageDigest md = MessageDigest.getInstance("MD5");
return new BigInteger(1, md.digest()).toString(16);
// Login.vue:206 —— 口令在前端先 MD5 再发送
loginParams.password = md5(values.password);
- 无盐 MD5 可用彩虹表/GPU 快速破解;所有账号共享同一哈希空间,泄露一个库全部可离线爆破。
- 前端 MD5 不能替代 HTTPS/加盐哈希,反而把"前端已知的哈希"变成凭证,重放即可登录(配合
/user/info?id=拿到的散列,无需明文即可登录任意账号)。
修复建议: 服务端采用带盐慢哈希(BCrypt/Argon2);口令不落库明文/前端散列;全站强制 HTTPS。
5.7 租户隔离失效
位置: config/TenantConfig.java + utils/Tools.java:619(getTenantIdByToken)
// token 格式: UUID_tenantId
// 无 "_" → 返回 0 → 超级管理员 → TenantSqlParser 返回 null → 不注入 tenant_id 过滤
- 多租户过滤完全依赖"从客户端 token 后缀解析 tenantId";解析不到则按超级管理员处理,不加任何过滤。
jsh_platform_config/jsh_tenant/jsh_sequence/jsh_function/jsh_sys_dict_*表被doTableFilter排除在租户过滤之外,任何租户用户都可读全局配置/字典/功能菜单。- 叠加 §4.4,跨租户数据访问基本畅通。
修复建议: tenantId 由服务端从会话/密钥派生而非 token 后缀;超管行为单独白名单化;平台配置类表按角色再过滤。
5.8 SQL 注入:当前未发现可利用注入点,存在放大器
结论:对全库 61 个 mapper XML 的 ${} 穷举 + Java 侧排查,未发现可利用的经典 SQL 注入。该 fork 已对历史 CVE 做了系统性加固:
${} 类型 |
数量 | 来源 | 判定 |
|---|---|---|---|
${criterion.condition} |
232 处 | MyBatis Generator 生成,算子硬编码(addCriterion("id =", ...) 为 protected),值走 #{} |
安全 |
${orderByClause} |
29 处 | service 层 17 处 setOrderByClause 全部为字符串常量("sort asc, id desc" 等),无请求参数 |
安全 |
${column} / ${order} |
各 4 处 | 前端可控,但 Controller 先过 StringUtil.safeSqlParse(),XML 内 OGNL 白名单(仅 5 个固定列名、asc/desc) |
安全 |
Java 侧:无 @Select 注解内嵌 SQL、无字符串拼接 SQL、无 MyBatis-Plus 原生 Wrapper 用户可控调用、无原生 JDBC;ids 批量经 StringUtil.strToLongList() 转 Long;分页排序经 SqlUtil.escapeOrderBySql()(白名单正则 [a-zA-Z0-9_\ ,\.]+,长度≤500)过滤。
真实风险(加固项): application.yml:17 的 allowMultiQueries=true 允许一条语句内堆叠执行多条 SQL。当前虽无已利用注入点,但一旦未来出现任何注入(回归、二次注入、插件/租户模块),即可从"只读 SELECT"升级为"堆叠 INSERT/UPDATE/DELETE/DDL 接管数据库"。
修复建议: allowMultiQueries=false;保留 #{} + 白名单体系;对 safeSqlParse 覆盖所有 Ex 查询入口做回归测试。
6. 低危 / 理论风险
6.1 fastjson 1.2.83 反序列化 sink(理论)
utils/ExtJsonUtils.java:99 在每次 API 响应序列化时对实体 ext 字段执行 JSON.parseObject((String) value)(单参,数据可来自用户输入)。全库未发现 setAutoTypeSupport(true)/addAccept/setSafeMode(false),1.2.83 默认 autoType 关闭,当前 @type 载荷会抛 AutoTypeNotSupportException。若未来升级 fastjson 或开启 autoType,即成 RCE sink。 建议后续升级 2.x 或切换 Jackson。
6.2 开放重定向
jshERP-web/src/permission.js:52-53 登录后 next({ path: decodeURIComponent(from.query.redirect) }) 按 URL 参数跳转,可被引导到任意站(钓鱼)。建议限制为站内路径白名单。
6.3 过期依赖
commons-io 1.3.2(2011 年,pom.xml:116-120):存在已知 CVE,本库无用户输入直接驱动的调用点。spring-boot-starter-redis 1.4.1、swagger-bootstrap-ui 1.6、SpringBoot 2.0.0(2018 年):整体框架版本过老,建议升级 LTS。
6.4 排查后确认非漏洞项(避免误报)
utils/ComputerInfo.java:50Runtime.getRuntime().exec(command):command 为硬编码数组(ipconfig/ifconfig),不受用户输入控制。- 通用文件上传
SystemConfigService.uploadLocal/uploadAliOss:已做bizPath防穿越 + 扩展名白名单(无 jsp 等可执行类型)+ 文件名重命名(时间戳),不可写 webshell。 - Excel 导入(jxl 内存解析,不落盘):仅校验
.xls扩展名,无写入面。 /systemConfig/static路径穿越:被过滤器 +replace("..","")双重抑制,读取范围被限制在上传根目录内,非任意文件读取。
7. 修复优先级建议
P0(立即,封堵 RCE 与提权链)
- 修复 §4.2
updateUser/resetPwd越权:写接口强制归属校验,敏感字段(id/password/loginName/roleId)不允许请求体直接映射。 - 修复 §4.3 角色绑定提权:
updateUserBusiness仅超管可用。 - 收紧 §4.1 插件上传:默认关闭插件动态加载,接口加签名/二次认证/受信来源。
- 关闭 §5.8
allowMultiQueries,消除堆叠注入放大器。 - 修改默认口令并强制开启验证码/限流;撤销初始化 SQL 中弱口令。
P1(尽快,消除未授权与数据泄露)
- 把 swagger、
/systemConfig/static/**、registerUser从匿名白名单移除;后端校验register_flag。 - 修复 §4.7
/user/info?id=返回口令散列;出参统一置空password。 - 修复 §4.6 任意文件删除:路径规范化 + 上传根目录强制校验。
- 修复 §5.2 存储型 XSS、§5.3
eval。
P2(规划加固)
- 口令哈希改 BCrypt/Argon2,全站 HTTPS,前端去除 MD5。
- 服务端出站请求加 SSRF 防护;敏感平台配置不外泄。
- 租户隔离改服务端派生 tenantId,去除"无后缀即超管"逻辑。
- 升级依赖(SpringBoot LTS、fastjson→Jackson、commons-io)。
8. 附录:关键文件索引
| 关注点 | 文件 |
|---|---|
| 认证过滤器/匿名白名单 | jshERP-boot/src/main/java/com/jsh/erp/filter/LogCostFilter.java |
| 租户解析 | .../config/TenantConfig.java、.../utils/Tools.java:619 |
| 用户越权写 | .../service/UserService.java(updateUser/resetPwd/registerUser) |
| 角色绑定提权 | .../service/UserBusinessService.java(updateUserBusiness) |
| 插件 RCE | .../controller/PluginController.java |
| 文件删除穿越 | .../service/SystemConfigService.java:350-381 |
| 未授权文件读取 | .../controller/SystemConfigController.java:228-286 |
| SSRF 出站请求 | .../utils/HttpClient.java、.../service/UserService.java:958-988 |
| fastjson sink | .../utils/ExtJsonUtils.java:99 |
| 无盐 MD5 | .../utils/Tools.java:582-588 |
| swagger | .../config/Swagger2Config.java |
| XSS 前端 sink | jshERP-web/src/components/tools/ShowAnnouncement.vue:19 |
| eval 注入 | jshERP-web/src/utils/util.js:113 |
| 开放重定向 | jshERP-web/src/permission.js:52-53 |
| 硬编码凭据 | jshERP-boot/src/main/resources/application.yml、docs/jsh_erp.sql |
本报告由只读代码审计生成,审计过程未修改、未执行、未部署任何代码。所有利用链均为理论分析,未在实际目标上验证。