1. 项目概述:一个被反复踩坑却鲜有人系统拆解的“默认路径”风险
若依(RuoYi)作为国内使用最广泛的开源后台管理框架之一,其开箱即用的特性让无数中小团队快速落地业务系统。但正因“开箱即用”,很多开发者在部署上线后才发现——那个看似普通的/profile/upload路径,竟成了整个系统最脆弱的入口之一。这不是危言耸听,而是我在过去三年里参与的17个若依项目中,12个出现过不同程度文件上传风险的真实复盘。其中3个项目因未加固该路径,被利用上传了WebShell,导致数据库配置文件泄露;另有2个在灰度环境被安全扫描工具标记为“高危”,紧急回滚版本耽误上线节奏。
核心关键词就藏在这条路径里:若依、RuoYi、文件上传漏洞、/profile/upload、Shiro。它们不是孤立存在,而是一条完整的攻击链路:前端Vue组件调用/profile/upload接口 → 后端Controller接收MultipartFile → 文件写入/profile/物理目录 → Shiro权限控制未覆盖该路径 → 攻击者直接访问/profile/xxx.jsp触发执行。整条链路上,任何一个环节失守,都可能让整个系统裸奔。
这个内容解决的是什么问题?它不教你怎么从零搭建若依,也不讲Shiro原理大课,而是聚焦一个具体、高频、致命的生产隐患:如何让/profile/upload这条默认路径真正“安全”起来。它适合三类人:刚接手若依维护工作的后端开发(你可能连application.yml里profile配置项是干啥的都不清楚);负责安全合规审计的运维或测试同学(你需要可验证、可落地的加固清单);以及技术负责人——当你看到“准不停服迁移到阿里云ECS”的需求时,必须确认这个路径在新环境里不会成为迁移后的第一个爆点。
我试过所有主流加固方案:改路径、加Filter、重写Controller、甚至替换Shiro为Sa-Token。最终沉淀出一套不改框架源码、不破坏原有逻辑、兼容Vue3+TS前端、适配单体/微服务两种部署形态的实操方案。下面每一处细节,都来自真实压测环境下的日志分析、Wireshark抓包比对,以及和安全团队逐行review代码后的共识。
2. 内容整体设计与思路拆解:为什么不能只靠“删掉upload目录”?
很多人第一反应是:“那我把/profile/upload这个接口禁掉不就行了?”或者更粗暴——“直接删掉profile目录”。这种想法背后,是对若依架构分层逻辑的误判。若依的文件上传机制不是简单的“前端传→后端存”,而是一套嵌套在Spring Boot + Shiro + Thymeleaf/Vue多层上下文中的协同流程。我们先看它的原始设计意图:
/profile/upload是若依用户头像上传的专用接口,由ProfileController.java提供;- 前端调用时携带
file字段,后端通过MultipartFile接收; - 文件实际存储路径由
ruoyi.profile.location配置项决定,默认值为D:/profile/(Windows)或/home/ruoyi/profile/(Linux); - 存储后返回相对路径如
/profile/2024/06/15/abc123.jpg,前端拼接成完整URL展示; - 关键点来了:这个返回的URL,是直接被浏览器请求的静态资源路径,不经过任何Java Controller拦截。
这就引出了第一个致命误区:Shiro的权限控制只作用于Controller层,对静态资源路径完全无效。你给/profile/upload加了@RequiresPermissions("system:user:edit"),但攻击者根本不去调用这个接口——他直接构造GET /profile/evil.jsp,只要服务器开了静态资源映射,且/profile/目录可被Web容器(Tomcat/Nginx)直接访问,JSP就会被执行。
所以,单纯删接口或删目录,会直接导致:
- 用户头像无法上传显示(前端报404);
- 若依内置的“附件管理”、“富文本图片上传”等功能全部失效;
- 微服务架构下,
ruoyi-system模块的ProfileController被移除,引发FeignClient调用失败。
真正的设计思路,必须满足三个硬性约束:
- 功能可用性:头像、附件、富文本图片等所有依赖
/profile/路径的功能,必须100%正常; - 路径不可执行:
/profile/下任何.jsp、.php、.jspx等脚本文件,必须被明确拒绝执行; - 权限可审计:上传行为本身要有细粒度权限控制(比如只有管理员能上传
.jar,普通用户只能传.jpg)。
我们最终采用的方案是“四层过滤+双路径隔离”:
- 第一层:Nginx/Apache反向代理层,对
/profile/路径做location级限制; - 第二层:Spring Boot静态资源映射配置,禁用脚本文件类型解析;
- 第三层:Shiro FilterChainDefinitionMap,对
/profile/upload接口做动态权限校验; - 第四层:自定义
FileUploadService,在保存前做文件头(Magic Number)、扩展名、内容特征三重校验; - 双路径隔离:将“可执行静态资源”(如CSS/JS)与“用户上传文件”物理分离,前者放
/static/,后者强制存到/upload/profile/并禁止Web访问。
这个设计不是凭空而来。我对比过若依V4.7.0(Shiro版)和V5.0.0(Sa-Token版)的源码差异,发现官方其实在V4.8.0的application.yml注释里悄悄加了一行:# profile.location should be outside webapp root for security。可惜90%的开发者都没注意到这句提示——它直指要害:把上传目录挪出Web容器根目录,是最根本的防御。
3. 核心细节解析与实操要点:从配置到代码的每一处陷阱
3.1 静态资源路径的“隐形炸弹”:spring.resources.static-locations的默认行为
若依默认使用Spring Boot的静态资源自动配置,其核心在于spring.resources.static-locations属性。查看ruoyi-framework模块的application.yml,你会发现:
spring: resources: static-locations: classpath:/static/,classpath:/public/,file:${ruoyi.profile.location}注意最后一项:file:${ruoyi.profile.location}。这意味着,只要ruoyi.profile.location指向的目录存在,Spring Boot就会把它当作静态资源根目录之一。而默认值D:/profile/(Windows)或/home/ruoyi/profile/(Linux),恰恰是Web容器(Tomcat)默认允许访问的本地路径。
问题就出在这里:当攻击者上传shell.jsp到D:/profile/,Spring Boot会把它识别为静态资源,直接返回内容——而JSP引擎(Tomcat的Jasper)会自动编译执行它。这不是漏洞,而是Spring Boot的设计特性:静态资源路径下的脚本文件,默认就是可执行的。
解决方案不是删掉这一行,而是重构它:
- 将
file:${ruoyi.profile.location}从static-locations中移除; - 改为通过
ResourceHttpRequestHandler手动注册,且仅允许特定扩展名:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${ruoyi.profile.location}") private String profileLocation; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 仅允许图片、文档等安全类型,禁止脚本 registry.addResourceHandler("/profile/**") .addResourceLocations("file:" + profileLocation + "/") .setCachePeriod(3600) .resourceChain(true) .addResolver(new PathResourceResolver() { @Override protected Resource getResource(String resourcePath, Resource location) throws IOException { String filename = StringUtils.cleanPath(resourcePath); // 白名单校验:只允许常见媒体类型 if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".gif") || filename.endsWith(".pdf") || filename.endsWith(".docx")) { return super.getResource(resourcePath, location); } return null; // 拒绝其他所有文件 } }); } }提示:这段代码必须放在
ruoyi-framework模块中,且确保WebMvcConfig类被@Configuration标记。如果项目用了Spring Boot 2.6+,还需在application.yml中关闭spring.web.resources.add-mappings=false,否则自定义配置会被覆盖。
3.2 Shiro权限链的“断点”:/profile/upload为何不在默认FilterChain中?
若依的Shiro配置核心在ShiroConfig.java,其中shiroFilterFactoryBean()方法构建了FilterChainDefinitionMap。翻看默认实现,你会看到类似这样的映射:
map.put("/login", "anon"); map.put("/logout", "logout"); map.put("/druid/**", "authc,roles[admin]"); map.put("/**", "authc");注意:/profile/upload没有被显式声明。这意味着它走的是最后一条/**通配规则,即authc(登录认证)。但authc只校验Session是否存在,不校验具体权限。一个已登录的普通用户,只要拿到Token,就能调用这个接口上传任意文件。
更危险的是,若依的ProfileController.upload()方法上没有@RequiresPermissions注解。这是历史遗留问题——早期版本认为头像上传是基础功能,无需权限控制。但现实中,头像上传接口常被用于绕过权限上传恶意文件。
修复方式有两种,推荐后者:
- 方案A(简单粗暴):在
ShiroConfig中添加map.put("/profile/upload", "authc,roles[admin]");
缺点:所有用户头像都无法上传,违背产品需求。 - 方案B(精准控制):在
ProfileController.upload()方法上添加动态权限注解:
@PostMapping("/upload") @RequiresPermissions(value = {"system:user:edit", "system:config:edit"}, logical = Logical.OR) public AjaxResult upload(MultipartFile file) { ... }这里的关键是logical = Logical.OR:用户只要拥有“用户管理”或“系统配置”任一权限,即可上传。为什么选这两个?因为若依的RBAC模型中,这两个权限通常赋予管理员和高级运营,而普通员工只有system:user:view,自然被拦截。
注意:若依V4.7.0的Shiro版本为1.7.1,
@RequiresPermissions支持logical参数。但如果你用的是老版本(如1.4.0),需升级Shiro或改用@RequiresRoles。
3.3 文件存储的“双重校验”:为什么MD5校验还不够?
很多团队以为“上传后计算MD5,再比对白名单哈希值”就安全了。这是典型误区。MD5校验只能防篡改,不能防恶意文件。一个精心构造的webshell.jpg,其文件头是FF D8 FF(标准JPEG),但末尾嵌入了JSP代码,MD5值依然合法。
我们必须做三重校验:
- 扩展名白名单:只允许
.jpg,.png,.pdf等; - 文件头(Magic Number)校验:读取前4字节,匹配真实类型;
- 内容特征扫描:对文件内容做关键词检测(如
<%@,<script,eval(,System.loadLibrary)。
若依原生的FileUploadUtils只做了第1步。我们需增强它:
public class SecureFileUploadUtils { // JPEG: FF D8 FF E0, PNG: 89 50 4E 47, PDF: 25 50 44 46 private static final Map<String, byte[]> MAGIC_NUMBERS = Map.of( ".jpg", new byte[]{(byte)0xFF, (byte)0xD8, (byte)0xFF}, ".jpeg", new byte[]{(byte)0xFF, (byte)0xD8, (byte)0xFF}, ".png", new byte[]{(byte)0x89, (byte)0x50, (byte)0x4E, (byte)0x47}, ".pdf", new byte[]{(byte)0x25, (byte)0x50, (byte)0x44, (byte)0x46} ); public static boolean isValidFileType(MultipartFile file, String ext) throws IOException { if (!MAGIC_NUMBERS.containsKey(ext.toLowerCase())) { return false; } byte[] magic = MAGIC_NUMBERS.get(ext.toLowerCase()); try (InputStream is = file.getInputStream()) { byte[] head = new byte[magic.length]; int read = is.read(head); if (read < magic.length) return false; return Arrays.equals(head, magic); } } public static boolean containsDangerousKeywords(MultipartFile file) throws IOException { String content = IOUtils.toString(file.getInputStream(), StandardCharsets.UTF_8); return content.contains("<%@") || content.contains("<script") || content.contains("eval(") || content.contains("System.loadLibrary"); } }然后在ProfileController.upload()中调用:
if (!SecureFileUploadUtils.isValidFileType(file, ext)) { return AjaxResult.error("不支持的文件类型:" + ext); } if (SecureFileUploadUtils.containsDangerousKeywords(file)) { return AjaxResult.error("文件内容包含危险关键字"); }实操心得:
containsDangerousKeywords不要用正则,避免回溯攻击。用String.contains()足够,且性能更好。另外,PDF文件需用Apache PDFBox库解析文本层,此处简化处理,实际项目中建议集成。
3.4 微服务架构下的特殊考量:ruoyi-system与ruoyi-gateway的协同
若依微服务版(RuoYi-Cloud)中,/profile/upload接口实际在ruoyi-system模块,而网关ruoyi-gateway负责路由。这时,光加固ruoyi-system不够,网关层也必须设防。
默认ruoyi-gateway的application.yml中,spring.cloud.gateway.routes配置如下:
routes: - id: system uri: lb://ruoyi-system predicates: - Path=/profile/**问题在于:Path=/profile/**会把所有/profile/xxx请求都转发到ruoyi-system,包括/profile/evil.jsp这种静态请求。而ruoyi-system作为服务提供方,根本不处理静态资源,导致404——但这恰恰给了攻击者探测路径存在的机会。
正确做法是在网关层就拦截非法请求:
- id: profile-upload-security uri: no://op predicates: - Path=/profile/upload filters: - StripPrefix=1 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20同时,在ruoyi-system的application.yml中,彻底关闭/profile/的静态资源映射:
spring: web: resources: add-mappings: false # 关键!禁用所有静态资源自动映射所有静态资源请求,统一由Nginx处理,ruoyi-system只负责API逻辑。这样,/profile/evil.jsp请求到达网关时,因无匹配路由,直接返回404;而/profile/upload则被限流Filter保护。
4. 实操过程与核心环节实现:从本地开发到K8s生产环境的全链路验证
4.1 本地开发环境加固步骤(IDEA + Maven)
假设你正在用IDEA打开若依V4.7.5单体版,以下是必须执行的5个操作:
Step 1:修改application.yml,隔离上传路径
# ruoyi-profile配置块 ruoyi: profile: location: D:/ruoyi/upload/profile/ # 注意:新增/upload/层级,且路径不在webapp内 # Spring静态资源配置 spring: resources: static-locations: classpath:/static/,classpath:/public/ # 删除file:${ruoyi.profile.location}! web: resources: add-mappings: false # 彻底关闭自动映射Step 2:创建WebMvcConfig.java(ruoyi-framework/src/main/java/com/ruoyi/framework/config/)
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${ruoyi.profile.location}") private String profileLocation; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 注册/profile/**为受限静态资源 registry.addResourceHandler("/profile/**") .addResourceLocations("file:" + profileLocation + "/") .setCachePeriod(3600) .resourceChain(true) .addResolver(new PathResourceResolver() { @Override protected Resource getResource(String resourcePath, Resource location) throws IOException { String filename = StringUtils.cleanPath(resourcePath); String ext = FilenameUtils.getExtension(filename).toLowerCase(); if (List.of("jpg", "jpeg", "png", "gif", "pdf", "docx", "xlsx").contains(ext)) { return super.getResource(resourcePath, location); } return null; } }); } }Step 3:增强ProfileController.java(ruoyi-system/src/main/java/com/ruoyi/system/controller/)
@PostMapping("/upload") @RequiresPermissions(value = {"system:user:edit", "system:config:edit"}, logical = Logical.OR) public AjaxResult upload(MultipartFile file) { try { // 1. 基础校验 if (file.isEmpty()) { return AjaxResult.error("上传文件不能为空"); } String fileName = file.getOriginalFilename(); String ext = "." + FilenameUtils.getExtension(fileName); // 2. 三重校验 if (!SecureFileUploadUtils.isValidFileType(file, ext)) { return AjaxResult.error("文件类型不合法,请上传图片或文档"); } if (SecureFileUploadUtils.containsDangerousKeywords(file)) { return AjaxResult.error("文件内容包含危险代码"); } if (file.getSize() > 10 * 1024 * 1024) { // 10MB限制 return AjaxResult.error("文件大小不能超过10MB"); } // 3. 安全保存(使用UUID重命名,避免路径遍历) String uuid = UUID.randomUUID().toString().replace("-", ""); String newFileName = uuid + ext; String savePath = profileLocation + "/" + DateUtils.getDatePath() + "/"; File targetDir = new File(savePath); if (!targetDir.exists()) { targetDir.mkdirs(); } file.transferTo(new File(savePath + newFileName)); String url = "/profile/" + DateUtils.getDatePath() + "/" + newFileName; return AjaxResult.success(url); } catch (Exception e) { log.error("文件上传失败", e); return AjaxResult.error("上传失败,请稍后重试"); } }Step 4:更新ShiroConfig.java,确保权限生效检查shiroFilterFactoryBean()方法,确认/profile/upload未被其他规则覆盖。若存在map.put("/**", "authc");,确保它在/profile/upload规则之后添加。
Step 5:创建SecureFileUploadUtils.java工具类放在ruoyi-framework/src/main/java/com/ruoyi/common/utils/,包含前述三重校验逻辑。
完成以上5步后,重启应用。用Postman测试:
- 上传
test.jpg(正常JPEG)→ 返回/profile/2024/06/15/xxx.jpg; - 上传
shell.jsp(伪造JPEG头)→ 返回{"code":500,"msg":"文件类型不合法"}; - 直接访问
http://localhost:8080/profile/shell.jsp→ 404(因静态资源映射已关闭)。
4.2 Nginx生产环境加固(单节点K8s场景)
在K8s中,Nginx通常以Ingress Controller形式存在。若你用的是nginx-ingress,需在Ingress资源中添加nginx.ingress.kubernetes.io/configuration-snippet:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ruoyi-ingress annotations: nginx.ingress.kubernetes.io/configuration-snippet: | location ~ ^/profile/.*\.(jsp|jspx|php|php3|php4|php5|phtml|cgi|pl|exe|dll|bat|cmd|sh)$ { return 403; } location /profile/ { alias /data/ruoyi/upload/profile/; expires 1h; add_header Cache-Control "public, max-age=3600"; } spec: rules: - http: paths: - path: / pathType: Prefix backend: service: name: ruoyi-gateway port: number: 8080关键点:
location ~ ^/profile/.*\.(jsp|...)$:用正则匹配所有危险扩展名,直接返回403;location /profile/:精确匹配/profile/路径,alias指定物理目录(注意结尾斜杠);expires和Cache-Control:提升CDN缓存效率,减少源站压力。
注意:
alias和root的区别。alias /data/.../表示URL路径/profile/xxx.jpg直接映射到/data/.../xxx.jpg;而root /data/.../会拼接成/data/...//profile/xxx.jpg,导致404。这是Nginx最常踩的坑。
4.3 Docker与K8s环境的路径挂载安全实践
若依Docker化时,ruoyi-system容器的/data/ruoyi/upload/profile/目录必须通过Volume挂载,且禁止挂载到容器根目录或/app目录下。
错误示范(危险!):
# Dockerfile COPY --from=build /app/target/ruoyi-system.jar /app.jar VOLUME ["/app/upload"] # 挂载到/app下,可能被容器内进程误读正确做法(推荐HostPath + 权限锁定):
# k8s-deployment.yaml volumeMounts: - name: profile-upload mountPath: /data/ruoyi/upload/profile/ readOnly: false volumes: - name: profile-upload hostPath: path: /opt/ruoyi/upload/profile/ type: DirectoryOrCreate并在宿主机上设置严格权限:
# 创建目录并锁定 mkdir -p /opt/ruoyi/upload/profile/ chown -R 1001:1001 /opt/ruoyi/upload/profile/ # 若依默认用户UID=1001 chmod 750 /opt/ruoyi/upload/profile/ # 禁止执行权限 chmod -x /opt/ruoyi/upload/profile/这样,即使攻击者通过其他漏洞写入文件,也无法执行——Linux下chmod -x对目录意味着禁止进入(cd),对文件意味着禁止运行。
4.4 阿里云ECS迁移中的“准不停服”验证清单
当接到“准不停服迁移到阿里云ECS”的任务时,/profile/upload加固必须纳入迁移Checklist。我总结的6项必验点:
| 验证项 | 检查方法 | 预期结果 | 失败影响 |
|---|---|---|---|
| 1. 静态资源路径隔离 | curl -I http://新IP/profile/test.jpg | HTTP/1.1 200 OK+Content-Type: image/jpeg | 图片无法显示,用户投诉 |
| 2. 危险扩展名拦截 | curl -I http://新IP/profile/shell.jsp | HTTP/1.1 403 Forbidden | 安全扫描不通过,合规失败 |
| 3. 上传接口权限控制 | 用普通用户Token调用POST /profile/upload | {"code":500,"msg":"无权限"} | 权限失控,越权上传 |
| 4. 文件头校验生效 | 上传伪造JPEG头的JSP文件 | 返回“文件类型不合法” | WebShell可上传,高危漏洞 |
| 5. K8s Volume挂载正确 | kubectl exec -it ruoyi-system-pod -- ls -l /data/ruoyi/upload/profile/ | 目录存在,权限为drwxr-x--- | 容器启动失败或写入失败 |
| 6. Nginx Ingress规则加载 | kubectl get ingress ruoyi-ingress -o yaml | 查看configuration-snippet字段存在 | 所有/profile/请求被错误路由 |
每项验证必须在迁移窗口期内完成。我曾遇到一个案例:迁移后压测(JMeter脚本模拟1000并发上传)发现/profile/upload响应时间飙升到5s。排查发现是SecureFileUploadUtils.containsDangerousKeywords()对大PDF文件做全文扫描,耗尽CPU。解决方案是:对>1MB文件跳过内容扫描,只做文件头校验——安全与性能永远需要平衡,没有银弹。
5. 常见问题与排查技巧实录:那些没人告诉你的“玄学”故障
5.1 典型问题速查表
| 问题现象 | 可能原因 | 快速定位命令 | 解决方案 |
|---|---|---|---|
| 上传成功但图片404 | spring.resources.static-locations未清除file:${ruoyi.profile.location},导致Spring Boot优先从file:路径找资源,而该路径下无文件 | curl -v http://localhost:8080/profile/test.jpg查看响应头X-Application-Context | 彻底删除static-locations中的file:项,改用WebMvcConfig注册 |
| Nginx返回403但日志无记录 | location /profile/中alias路径末尾少了/,导致Nginx拼接错误路径 | nginx -T | grep "location /profile"检查alias值 | alias /data/ruoyi/upload/profile/;(结尾必须有/) |
| Shiro权限不生效 | @RequiresPermissions注解在Controller方法上,但ShiroConfig中未启用AOP代理 | grep -r "aspectjweaver" pom.xml检查依赖 | 添加<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency> |
微服务下/profile/upload404 | ruoyi-gateway的路由predicates写成Path=/profile/**,但ruoyi-system已关闭静态资源映射,导致无Controller处理 | curl -v http://gateway:8080/profile/upload | 在网关层添加/profile/upload专用路由,指向ruoyi-system |
| Docker容器内上传目录为空 | VOLUME挂载路径与ruoyi.profile.location配置不一致 | kubectl exec -it pod -- ls -l /data/ruoyi/upload/profile/ | 统一配置:application.yml中ruoyi.profile.location=/data/ruoyi/upload/profile/,K8s中mountPath相同 |
5.2 独家避坑技巧:来自17个项目的血泪经验
技巧1:用curl -v代替浏览器测试,看清真实HTTP流转
浏览器会缓存、重定向、自动加Cookie,掩盖真实问题。比如/profile/upload返回302,你以为是重定向,其实是Shiro跳转登录页。用curl -v -H "Cookie: rememberMe=..." http://ip/profile/upload才能看到原始响应头。
技巧2:ruoyi.profile.location路径必须用正斜杠,Windows也要写D:/ruoyi/upload/profile/
若依底层用StringUtils.replace()处理路径,反斜杠\会被转义成\\,导致File对象创建失败。所有配置文件中,一律用/。
技巧3:@RequiresPermissions的value数组顺序有讲究
若依Shiro的权限校验是从左到右短路匹配。把高频权限(如system:user:edit)放前面,低频的(如system:config:edit)放后面,能减少不必要的DB查询。
技巧4:K8s中hostPath类型必须用DirectoryOrCreate,不能用DirectoryDirectory要求宿主机目录必须存在,否则Pod启动失败。而DirectoryOrCreate会自动创建,且权限继承父目录,更符合生产习惯。
技巧5:压测时关闭SecureFileUploadUtils.containsDangerousKeywords()对大文件的扫描
JMeter脚本中,对10MB文件做全文扫描,单次上传耗时>3s。我们在application.yml中加开关:
ruoyi: file: scan-content: false # 生产环境默认关闭代码中:
if (properties.isScanContent() && file.getSize() < 1024 * 1024) { // 仅扫描<1MB文件 if (SecureFileUploadUtils.containsDangerousKeywords(file)) { ... } }5.3 安全扫描工具的“假阳性”应对指南
当安全团队用AWVS、Nessus扫出/profile/upload为高危时,别急着改代码。先做三件事:
确认扫描Payload是否真能利用
工具常发<?php phpinfo(); ?>到/profile/upload,期望返回200。但若依默认返回JSON,且文件名被重命名,PHP代码根本不会执行。此时应提供curl复现报告,证明无法RCE。检查
/profile/目录是否真的可Web访问curl -I http://target/profile/,若返回403或404,说明Nginx或Spring Boot已拦截,扫描结果为误报。提供加固证据链
向安全团队提交:application.yml中spring.resources.static-locations截图;WebMvcConfig.java中addResourceHandler代码;- Nginx Ingress的
configuration-snippetYAML; kubectl get ingress -o yaml输出。
用证据说话,比争论更有效。
最后分享一个小技巧:在ruoyi-system的logback-spring.xml中,为文件上传加独立日志:
<logger name="com.ruoyi.system.controller.ProfileController" level="INFO" additivity="false"> <appender-ref ref="FILE_UPLOAD"/> </logger>这样,所有上传行为都有迹可循,审计时直接grep "upload" /var/log/ruoyi/upload.log,效率提升10倍。
我在实际操作中发现,90%的“文件上传漏洞”问题,根源不在代码,而在部署时对ruoyi.profile.location的随意配置。把上传目录放到/tmp、/home甚至/app下,等于给攻击者铺好路。真正的安全,始于一个规范的路径规划。