尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

若依框架安全加固指南:Shiro密钥、SQL注入与文件读取漏洞修复

若依框架安全加固指南:Shiro密钥、SQL注入与文件读取漏洞修复 1. 项目概述为什么若依框架需要一份专属的安全自查清单若依RuoYi作为国内Java开发者圈子里普及度极高的开源后台管理系统以其“开箱即用”的特性赢得了大量青睐。无论是单体版还是微服务版很多中小型项目甚至一些快速迭代的内部系统都直接基于若依进行二次开发。但“快”的另一面往往是安全细节的疏忽。我见过太多团队拿到若依后只关心业务功能是否跑通UI是否美观却很少系统地审视其默认配置下的安全隐患。直到某天被安全扫描工具打出一堆高危漏洞或者更糟——真的出了安全事件才手忙脚乱地开始“打补丁”。这份清单就是针对若依4.7.x版本一个非常经典且仍有大量项目在使用的版本的一次深度“体检”和“加固手术”。它聚焦于三个最典型、也最危险的漏洞类型Shiro默认密钥导致的身份绕过与反序列化、SQL注入、以及任意文件读取。这些漏洞并非若依独有但由于框架的默认配置和某些历史版本的代码逻辑它们在若依项目中的存在具有相当的普遍性。我们的目标不是制造恐慌而是提供一份清晰、可操作的自查与修复指南让你在享受若依开发便利的同时也能筑起一道可靠的安全防线。2. 核心漏洞原理与风险深度解析在动手修复之前我们必须理解这些漏洞究竟是如何产生的以及它们可能带来的实际危害。知其然更要知其所以然这样才能在后续的开发中举一反三避免引入同类问题。2.1 Shiro默认密钥一把人尽皆知的“家门钥匙”Apache Shiro是一个强大的Java安全框架若依用它来处理登录认证、权限控制和会话管理。Shiro有一个“记住我”RememberMe功能其核心是将用户的身份信息序列化后用AES算法加密存储在客户端的Cookie中。加解密需要一个密钥CipherKey。问题的根源在于若依在早期版本中使用了Shiro官方示例中硬编码的默认密钥kPHbIxk5D2deZiIxcaaaA。这个密钥在互联网上几乎是公开的“秘密”。攻击者一旦知道你的系统使用此密钥就可以伪造任意用户的“记住我”Cookie直接绕过登录以任何用户身份包括管理员进入系统。触发反序列化攻击如果项目中存在可利用的反序列化链依赖库如 Commons-Collections 的老版本攻击者可以构造恶意的序列化数据加密后作为Cookie发送服务器在解密反序列化时就可能执行任意代码完全控制服务器。这就像你家门锁的钥匙模具被公开挂在网上任何人都能轻易复制一把打开你的门。风险极高。2.2 SQL注入数据层的“万能钥匙”SQL注入是老生常谈但在若依的动态SQL拼接场景下依然常见。若依的MyBatis XML映射文件中大量使用了${}进行参数拼接而非安全的#{}预编译。${}与#{}的本质区别#{}MyBatis会将其处理为预编译语句的参数占位符?传入的参数会被视为一个整体字符串或数字从根本上杜绝了SQL注入。${}MyBatis会将其直接替换为字符串拼接进SQL语句。如果用户输入 or 11拼接后的SQL就可能改变原意。例如一个模糊查询!-- 危险写法 -- select idselectUser parameterTypeString resultMapUserResult select * from sys_user where user_name like %${userName}% /select如果userName传入 or 11最终SQL变为select * from sys_user where user_name like % or 11%导致查询出所有用户。风险场景若依后台的管理模块如用户管理、日志查询等凡涉及动态表头、排序字段、过滤条件且使用了${}的地方都是潜在的风险点。攻击者可以利用此漏洞窃取、篡改或删除数据库中的所有敏感数据。2.3 任意文件读取服务器的“后门”任意文件读取漏洞允许攻击者通过构造特定的请求路径读取服务器上的任意文件。在若依4.7.x的某些版本中此漏洞主要出现在两个地方定时任务Quartz的调用目标字符串invokeTarget校验不严在动态调用类方法时如果传入的字符串参数未做严格过滤攻击者可能通过../../../这样的目录遍历Path Traversal手法拼接到文件路径中从而读取系统敏感文件如/etc/passwd、C:\Windows\win.ini或应用的配置文件含数据库密码。文件下载/预览接口的路径参数未校验一些文件下载或图片预览接口直接从请求参数中获取文件路径如果没有校验该路径是否在允许的目录范围内攻击者同样可以跨目录读取文件。危害攻击者可以读取服务器上的配置文件获取数据库连接信息、加密密钥、源代码、日志文件可能包含敏感信息甚至系统关键文件为后续更深入的攻击如SSH密钥窃取铺平道路。3. 漏洞修复与系统加固实操指南理解了原理我们现在开始“动手术”。以下操作均基于若依4.7.x版本请根据你的实际代码情况进行调整。3.1 Shiro安全加固更换密钥与关闭风险功能第一步生成并替换一个全新的、强壮的Shiro密钥。绝对不要使用任何已知的默认密钥。我们可以在项目启动时动态生成一个密钥。找到Shiro配置类通常是ShiroConfig.java或RuoYiConfig.java中与Shiro相关的部分。修改RememberMe管理器配置定位到cookieRememberMeManager()或rememberMeManager()方法。使用随机生成的密钥将原有的固定密钥替换为如下代码import org.apache.shiro.crypto.AesCipherService; import java.util.Base64; Bean public CookieRememberMeManager rememberMeManager(){ CookieRememberMeManager cookieRememberMeManager new CookieRememberMeManager(); // 生成一个随机的128位16字节AES密钥 AesCipherService cipherService new AesCipherService(); byte[] key cipherService.generateNewKey().getEncoded(); String base64Key Base64.getEncoder().encodeToString(key); // 打印密钥首次启动时记录并妥善保存后续部署需使用同一密钥。 System.out.println(Generated Shiro RememberMe Key: base64Key); cookieRememberMeManager.setCipherKey(Base64.getDecoder().decode(base64Key)); // ... 其他配置 return cookieRememberMeManager; }重要提示首次启动后控制台会打印出新密钥。你必须将此密钥记录下来并作为配置文件如application.yml中的一个项后续部署从配置读取而不是每次启动都重新生成。否则所有已登录用户的“记住我”功能都会失效。第二步检查并关闭不必要的Shiro过滤器。在ShiroConfig的shiroFilter()方法中检查URL过滤规则。确保像/actuator/**、/druid/**、/swagger-ui.html这类监控或调试接口在生产环境中已被正确拦截或禁用避免信息泄露。第三步升级Shiro及相关依赖。检查pom.xml确保使用的Shiro版本至少是1.7.0以上以规避已知的旧版本反序列化漏洞。同时检查项目中是否存在已知的危险反序列化链依赖如commons-collections:commons-collections的3.x老版本考虑升级或排除。3.2 SQL注入全面防御MyBatis规范与全局过滤修复SQL注入需要代码层面的细致审查和习惯养成。1. 将${}替换为#{}治本之策这是最根本的解决方案。对所有MyBatis XML文件进行全局搜索$逐一审查。安全改造示例!-- 修复后 -- select idselectUser parameterTypeString resultMapUserResult select * from sys_user where user_name like concat(%, #{userName}, %) /select使用concat函数或bind标签来实现模糊查询。select idselectUser parameterTypeString resultMapUserResult bind nameuserNamePattern value% userName % / select * from sys_user where user_name like #{userNamePattern} /select2. 必须使用${}的场景严格的白名单校验对于动态表名、排序字段order by等无法使用预编译的场景必须进行严格的输入校验。创建工具类进行白名单校验public class SqlInjectionUtil { private static final SetString SAFE_SORT_FIELDS new HashSet(Arrays.asList(create_time, user_id, user_name)); private static final Pattern VALID_FIELD_PATTERN Pattern.compile(^[a-zA-Z0-9_]$); public static String checkSortField(String fieldName) { // 方案一白名单校验推荐 if (!SAFE_SORT_FIELDS.contains(fieldName)) { throw new IllegalArgumentException(非法的排序字段: fieldName); } // 方案二正则校验灵活性高但需确保正则严密 if (!VALID_FIELD_PATTERN.matcher(fieldName).matches()) { throw new IllegalArgumentException(非法的排序字段格式: fieldName); } return fieldName; } }在Service层调用在拼接SQL前先对传入的参数进行校验。String orderByField SqlInjectionUtil.checkSortField(params.get(orderByColumn)); // 然后再使用 ${orderByField}3. 启用MyBatis SQL拦截插件进行监控即使修复后也建议添加一个拦截器用于在开发测试阶段监控所有执行的SQL及时发现潜在的拼接风险。Intercepts({Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class})}) public class SqlInjectionInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler (StatementHandler) invocation.getTarget(); BoundSql boundSql handler.getBoundSql(); String sql boundSql.getSql(); // 简单的正则检测报警告日志生产环境可关闭或只记录错误 if (sql.matches(.*\\$\\{.*\\}.*)) { log.warn(⚠️ 警告执行的SQL中包含${}拼接请确认是否安全。SQL: {}, sql); } // 检测可疑的SQL片段 if (sql.toLowerCase().contains( or ) || sql.toLowerCase().contains(union select)) { log.error( 高危SQL语句中包含疑似注入关键词请立即审查SQL: {}, sql); } return invocation.proceed(); } } // 在MyBatis配置中注入此拦截器3.3 任意文件读取漏洞封堵路径校验与权限控制1. 修复定时任务Quartz文件读取漏洞此漏洞通常出现在JobInvokeUtil.java类中在通过反射调用方法时对传入的字符串参数特别是文件路径相关参数处理不当。修复关键在对invokeTarget调用目标字符串进行解析和执行前加入参数校验逻辑。特别是当参数表示文件路径时必须将其规范化为绝对路径并判断是否在允许的目录内。示例加固代码public static void invokeMethod(...) throws Exception { // ... 原有的反射调用准备代码 // 假设 methodParams 是调用方法的参数列表 for (Object param : methodParams) { if (param instanceof String) { String strParam (String) param; // 如果参数看起来像一个文件路径简单判断 if (strParam.contains(..) || strParam.startsWith(/) || strParam.matches(^[A-Za-z]:\\\\.*)) { // 进行规范化并校验 Path normalizedPath Paths.get(strParam).normalize().toAbsolutePath(); Path safeBasePath Paths.get(/app/safe/directory).toAbsolutePath(); // 你的安全基础目录 if (!normalizedPath.startsWith(safeBasePath)) { throw new SecurityException(禁止访问安全目录之外的文件路径: strParam); } // 可以用校验后的安全路径替换原参数 } } } // ... 执行反射调用 }2. 加固文件下载/预览接口所有从请求参数中接收文件路径的接口都必须加固。方案一白名单映射。不直接传递文件路径而是传递一个文件ID或经过编码的令牌后端通过ID从数据库查找到安全的存储路径。方案二强制校验与规范化。如果必须传路径则GetMapping(/common/download) public void download(String fileName, HttpServletResponse response) { // 1. 解码文件名如果前端编码了 String decodedFileName FileUtils.filenameFilter(fileName); // 自定义过滤函数移除../等 // 2. 拼接基础目录并规范化路径 Path baseDir Paths.get(D:/ruoyi/uploadPath).toAbsolutePath(); // 你的上传目录 Path filePath baseDir.resolve(decodedFileName).normalize(); // 3. 关键校验确保最终路径仍在基础目录内 if (!filePath.startsWith(baseDir)) { throw new RuntimeException(文件路径非法禁止目录遍历攻击。); } // 4. 检查文件是否存在然后下载... if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) { throw new RuntimeException(文件不存在。); } // ... 后续下载逻辑 }注意FileUtils.filenameFilter需要自己实现用于过滤../、..\、%2e%2e%2fURL编码等各种形式的目录遍历符。4. 进阶加固与安全开发规范完成上述紧急修复后我们需要建立长期的安全防线将安全思维融入日常开发。4.1 依赖组件安全管理定期升级依赖使用Maven的versions:display-dependency-updates插件或GitHub Dependabot、Snyk等工具定期扫描并升级项目依赖特别是Spring Boot、MyBatis、Shiro、Fastjson等核心组件。移除无用依赖清理pom.xml中与业务无关的依赖减少攻击面。比如一些旧的工具包可能包含已知漏洞。使用安全版本数据库参考国家信息安全漏洞库CNNVD或NVD了解所用组件的安全公告。4.2 应用层安全增强输入验证与输出编码Controller层验证使用JSR-303注解如NotBlankSize或Spring Validator进行强类型校验。输出编码在Thymeleaf或FreeMarker模板中默认启用HTML转义。向前端返回JSON数据时确保敏感信息如手机号、邮箱已脱敏。会话与权限细化会话超时在application.yml中配置合理的会话超时时间如server.servlet.session.timeout: 30m。权限注解除了Shiro自带的RequiresPermissions对于关键操作如删除、导出、资金操作可以增加自定义注解进行更细粒度的日志记录和二次确认。安全HTTP头通过配置WebSecurityConfigurerAdapter或使用spring-boot-starter-security即使不用它的认证功能来添加安全头部如Strict-Transport-Security(HSTS)强制HTTPS。X-Content-Type-Options: nosniff防止MIME类型嗅探。X-Frame-Options: DENY防止点击劫持。Content-Security-Policy(CSP)有效缓解XSS。4.3 部署与运维安全最小权限原则运行若依应用的系统账户不应具有root或管理员权限。只授予其必要的文件读写和网络访问权限。配置文件隔离将application-prod.yml中的数据库密码、加密密钥等敏感信息从代码仓库中分离。使用环境变量、配置中心或密钥管理服务来注入。定期安全扫描在CI/CD流水线中集成静态应用安全测试SAST工具如SonarQube、Fortify SCA或开源工具FindSecBugs对代码进行自动化漏洞扫描。同时使用动态应用安全测试DAST工具如OWASP ZAP对运行中的应用进行黑盒测试。日志与监控确保应用日志完整记录关键操作登录、增删改敏感数据和异常信息。对接监控告警平台对频繁的登录失败、异常的路径访问等行为设置告警。5. 常见问题排查与修复验证实录在实际加固过程中你可能会遇到以下问题Q1更换Shiro密钥后已登录用户的“记住我”功能全部失效怎么办A1这是预期行为。因为旧Cookie是用旧密钥加密的新密钥无法解密。解决方案有两种方案一推荐适用于用户量不大或可接受重新登录直接让所有用户重新登录。在升级公告中说明。方案二平滑过渡在代码中暂时保留解密逻辑先同时支持新旧两个密钥进行解密但加密只用新密钥。实现一个支持多密钥的RememberMeManager运行一段时间如一个月后再移除旧密钥支持。此方案较复杂需自定义Shiro组件。Q2将${}改为#{}后模糊查询like语句报错或查不出数据。A2这是最常见的语法错误。#{}会被当作字符串参数处理所以like %#{name}%实际会变成like %张三%语法错误。必须使用concat函数或bind标签具体写法见3.2节示例。Q3修复文件读取漏洞时路径规范化后在Windows和Linux系统下表现不一致。A3使用java.nio.file.Paths和Path.normalize()方法是平台无关的它会正确处理不同系统的路径分隔符。关键在于在路径校验前先将基础目录和用户输入路径都转换为绝对路径再进行startsWith比较这样可以避免因相对路径导致的校验绕过。Q4如何验证我的修复是否真的有效A4必须进行验证测试而非“我以为修好了”。Shiro密钥使用旧密钥生成的Cookie尝试访问应被拒绝跳转到登录页或报错。使用新密钥可通过登录后获取生成的Cookie可以正常访问。SQL注入手动测试在所有搜索、排序接口尝试输入、\、or 11--、;sleep(5)--等Payload观察响应。正常情况应返回空结果或明确的参数错误而不是执行了Payload或导致页面异常。工具扫描使用sqlmap等工具仅在授权测试环境下使用对修复后的接口进行扫描确认高危漏洞已消失。文件读取尝试访问../、..\、....//等变形Payload如/common/download?fileName../../../etc/passwd。尝试读取已知的、但不应被访问的服务器文件如应用的application.yml。所有此类请求都应返回“文件不存在”、“路径非法”等统一错误信息避免泄露服务器真实路径。Q5若依框架本身发布了新版本修复了这些漏洞我该直接升级吗A5升级是彻底解决问题的好方法但需谨慎评估。优点官方修复最权威通常能一次性解决多个问题。挑战若依版本间可能存在数据库结构变更、API变更或依赖库大版本升级直接覆盖升级可能导致你的二次开发代码不兼容。建议流程详细阅读目标版本的官方更新日志重点关注Breaking Changes破坏性变更。在本地或测试环境基于你的当前代码创建一个新分支尝试合并官方的新版本代码。这是一个复杂的代码合并过程可能需要手动解决大量冲突。进行全面的功能回归测试和性能测试。如果合并升级成本过高退而求其次可以只反向移植Backport官方针对特定漏洞的修复补丁到你的当前版本。这需要你具备一定的代码阅读和修改能力。安全加固不是一次性的任务而是一个持续的过程。这份清单为你修复若依4.7.x的高危漏洞提供了明确的路径但更重要的是它希望你建立起“默认不安全处处需验证”的安全开发意识。在后续的每一次功能开发、每一次第三方库引入、每一次接口暴露时都多问一句这里会不会有注入这个配置会不会太宽松这个权限是不是给大了唯有将安全作为开发流程中不可分割的一环你的系统才能真正稳固。
返回列表