RuoYi-v4.5.0 文件下载接口的坑:从一次调试到发现任意文件读取漏洞
从调试到发现RuoYi文件下载接口的路径拼接陷阱那天下午我正对着屏幕上的404错误发呆。项目里一个简单的文件下载功能突然罢工而日志里只有一句冷冰冰的File not found。作为团队里负责这个模块的开发者我不得不深入RuoYi框架的源码开始了一场意料之外的探险。1. 问题初现消失的文件事情始于一个再普通不过的需求——用户需要下载服务器上的资源文件。我们的系统基于RuoYi v4.5.0构建按照文档调用/common/download/resource接口应该就能完成这个功能。但当我传入/profile/download/test.pdf这样的参数时服务端却返回了文件不存在的错误。典型的调试过程开始了在CommonController类中添加System.out.println输出关键变量检查Global.getProfile()返回的本地路径是否正确验证StringUtils.substringAfter()方法的处理逻辑// 调试时添加的日志输出 System.out.println(localPath: localPath); // 输出D:/ruoyi/uploadPath/ System.out.println(resource参数: resource); // 输出/profile/download/test.pdf System.out.println(处理后路径: downloadPath); // 输出D:/ruoyi/uploadPath/download/test.pdf奇怪的是拼接后的路径看起来完全正确文件也确实存在于该位置。这让我意识到问题可能不在路径拼接本身而是文件读取的环节。2. 深入源码FileUtils的读写逻辑顺着调用链我找到了FileUtils.writeBytes方法。这个工具类方法负责将文件内容写入输出流是实际执行文件操作的关键位置。public static void writeBytes(String filePath, OutputStream os) throws IOException { FileInputStream fis null; try { File file new File(filePath); if (!file.exists()) { throw new FileNotFoundException(filePath); } fis new FileInputStream(file); byte[] b new byte[1024]; int length; while ((length fis.read(b)) 0) { os.write(b, 0, length); } } // ... 异常处理和资源关闭代码 }关键发现点方法直接使用传入的filePath创建File对象没有对路径进行规范化处理(normalize)没有检查路径是否在允许的目录范围内这让我警觉起来——如果我能控制filePath的值理论上可以读取服务器上的任意文件。3. 路径拼接的致命缺陷回到CommonController重点分析downloadPath的生成逻辑String localPath Global.getProfile(); // 固定配置D:/ruoyi/uploadPath/ String downloadPath localPath StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);StringUtils.substringAfter方法的作用是截取/profile/之后的部分。看起来安全的设计背后隐藏着危险输入resource值处理后结果实际访问路径/profile/test.txttest.txtD:/ruoyi/uploadPath/test.txt/profile/../application.yml../application.ymlD:/ruoyi/uploadPath/../application.yml/profile/../../pom.xml../../pom.xmlD:/ruoyi/uploadPath/../../pom.xml漏洞成因总结路径拼接前未对用户输入进行规范化处理未检查最终路径是否仍在允许的目录范围内依赖简单的字符串拼接而非安全的路径API4. 从理论到实践漏洞验证为了确认这个发现我设计了几组测试用例测试案例1读取同级目录文件GET /common/download/resource?resource/profile/../application.properties预期结果返回D:/ruoyi/uploadPath/../application.properties即D:/ruoyi/application.properties测试案例2读取上级目录文件GET /common/download/resource?resource/profile/../../pom.xml预期结果返回项目根目录下的pom.xml文件测试案例3读取系统敏感文件仅用于本地测试验证GET /common/download/resource?resource/profile/../../../../Windows/System32/drivers/etc/hosts注意实际渗透测试中尝试读取系统文件可能违反法律此处仅作技术讨论测试结果证实了我的猜想——通过精心构造的resource参数确实可以读取服务器上的任意文件包括项目配置文件application.yml, application.properties源代码文件.java, .xml系统敏感文件/etc/passwd, C:/Windows/win.ini等5. 修复方案与安全编码实践发现问题后我立即着手修复这个安全隐患。以下是几种可行的解决方案方案1路径规范化目录检查String safePath Paths.get(localPath) .resolve(StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX)) .normalize() .toString(); if (!safePath.startsWith(localPath)) { throw new SecurityException(非法路径访问); }方案2使用白名单校验文件名String filename StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); if (!filename.matches([a-zA-Z0-9-_]\\.[a-zA-Z0-9])) { throw new SecurityException(非法文件名); }更全面的安全建议输入验证对所有用户提供的路径参数进行严格校验路径规范化使用Paths.normalize()消除../等跳转序列目录限制确保最终路径位于允许的根目录下最小权限原则运行服务的账户应只有必要目录的读取权限日志监控记录所有文件下载请求特别关注异常路径6. 漏洞的深层思考这次调试经历让我对Web应用安全有了更深刻的认识。表面上看这只是一个简单的路径拼接问题但背后反映了几类常见的安全隐患安全反模式过度信任用户输入缺乏边界检查使用字符串操作代替专用路径API错误的安全假设认为前缀检查足够防御性编程要点不信任原则所有外部输入都应视为不可信的明确边界定义清晰的资源访问边界并严格执行深度防御多层防护比单一检查更可靠安全默认值失败时应拒绝而非允许这次调试意外演变成安全审计的经历让我明白了一个道理在软件开发中我们不仅要让代码能工作更要让代码安全地工作。每一个看似无害的字符串拼接每一次对用户输入的信任都可能成为系统安全的阿喀琉斯之踵。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2445377.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!