当前位置: 首页 > 图灵资讯 > 技术篇> 项目讲解之常见安全漏洞

项目讲解之常见安全漏洞

来源:图灵教育
时间:2023-04-19 16:19:13

  本文从开源项目开始 RuoYi 根据关键词漏洞|安全|防止筛选的提交记录文本描述。旨在介绍日常项目开发中需要注意的一些安全问题以及如何解决。

  项目安全是每个开发人员都需要关注的问题。如果项目漏洞太多,很容易受到黑客的影响 attack 用户信息泄露的风险。本文将结合三个典型案例,解释常见的安全漏洞和修复方案,帮助您在项目开发中进一步提高安全意识。Ruoyi项目地址:https://gitee.com/y_project/RuoYi 博主github地址:https://github.com/wayn111,欢迎关注 一、重置用户密码

  RuoYi 在提交记录时,项目中有一个重置用户密码的接口 dd37524b 以前的代码如下: @Log(title = “重置密码”, businessType = BusinessType.UPDATE)@PostMapping("/resetPwd")@ResponseBodypublic AjaxResult resetPwd(SysUser user){ user.setSalt(ShiroUtils.randomSalt()); user.setPassword(passwordService.encryptPassword(user.getLoginName(), user.getPassword(), user.getSalt())); int rows = userService.resetUserPwd(user); if (rows > 0) { setSysUser(userService.selectUserById(user.getUserId())); return success(); } return error();}

  可以看出,接口将读取输入的用户信息。重置用户密码后,根据输入 userId 更新数据库和缓存。

  如果盲目相信用户信息,这里有一个非常严重的安全问题 attack 人员通过接口结构要求并进入 user 参数中设置 userId 对于其他用户 userId,然后这个接口会导致一些用户的密码被重置 attack 人员掌握。1.1 attack 流程

  假如 attack 掌握其他用户的人员 userId 以及登录帐号名 结构重置密码请求 将 userId 没有其他用户的设置 userId 根据传入的服务端 userId 修改用户密码 使用新的用户帐户和重置的密码登录 attack 成功 1.2 如何解决

  在记录 dd37524b 代码更新如下: @Log(title = “重置密码”, businessType = BusinessType.UPDATE)@PostMapping("/resetPwd")@ResponseBodypublic AjaxResult resetPwd(String oldPassword, String newPassword){ SysUser user = getSysUser(); if (StringUtils.isNotEmpty(newPassword) && passwordService.matches(user, oldPassword)) { user.setSalt(ShiroUtils.randomSalt()); user.setPassword(passwordService.encryptPassword( user.getLoginName(), newPassword, user.getSalt())); if (userService.resetUserPwd(user) > 0) { setSysUser(userService.selectUserById(user.getUserId())); return success(); } return error(); } else { return error(”密码修改失败,旧密码错误”); }}

  解决方案其实很简单。不要盲目相信用户引入的参数,通过登录状态获取当前登录用户的userID。如上代码所示 getSysUser() 获取当前登录用户的方法。 userId 后,再根据 userId 重置密码。二、下载文件

  下载文件 web 相信大家都很熟悉开发中每个项目都会遇到的功能。RuoYi 在提交记录 18f6366f 以前的下载文件逻辑如下: @GetMapping("common/download")public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request){ try { if (!FileUtils.isValidFilename(fileName)) { throw new Exception(StringUtils.format( 文件名称({})非法,不允许下载。 ", fileName)); } String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); String filePath = Global.getDownloadPath() + fileName; response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); FileUtils.setAttachmentResponseHeader(response, realFileName); FileUtils.writeBytes(filePath, response.getOutputStream()); if (delete) { FileUtils.deleteFile(filePath); } } catch (Exception e) { log.error(“下载文件失败”, e); }}public class FileUtils{ public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\\\\\\\\\\\\\.\u4e00-\u9fa5+"; public static boolean isValidFilename(String filename) { return filename.matches(FILENAME_PATTERN); }}

  在代码中下载文件时,可以看到文件名称是否合法。如果不合法,会提示 文件名称({})非法,不允许下载。 单词。乍一看,这似乎没有问题。博客公司项目中下载的文件也有类似的代码。输入下载文件的名称,然后在指定的目录中找到要下载的文件,并通过流回写给客户端。

  在这种情况下,让我们看看提交记录 18f6366f 描述信息,

项目解释中常见的安全漏洞_下载文件

  我不知道。我一看就吓了一跳。原来,在此提交之前,项目中存在任何文件下载漏洞。在这里,博主将解释为什么任何文件下载漏洞。2.1 attack 流程

  假设下载目录为 /data/upload/ 结构下载文件请求 设置下载文件名称为:../../home/重要文件.txt 服务端将文件名与下载目录拼接,实际下载文件的完整路径为 /data/upload/../../home/重要文件.txt 包含下载文件 .. 字符将执行跳跃目录的逻辑 上跳目录逻辑执行后,实际下载文件为 /home/重要文件.txt attack 成功 2.2 如何解决

  让我们来看看提交记录 18f6366f 代码如下: @GetMapping("common/download")public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request){ try { if (!FileUtils.checkAllowDownload(fileName)) { throw new Exception(StringUtils.format( 文件名称({})非法,不允许下载。 ", fileName)); } String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); String filePath = Global.getDownloadPath() + fileName; response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); FileUtils.setAttachmentResponseHeader(response, realFileName); FileUtils.writeBytes(filePath, response.getOutputStream()); if (delete) { FileUtils.deleteFile(filePath); } } catch (Exception e) { log.error(“下载文件失败”, e); }}public class FileUtils{ /** * 检查文件是否可以下载 * * @param resource 需要下载的文件 * @return true 正常 false 非法 */ public static boolean checkAllowDownload(String resource) { // 禁止在目录中跳跃 if (StringUtils.contains(resource, "..")) { return false; } // 检查允许下载的文件规则 if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) { return true; } // 不允许下载的文件规则 return false; }}...public static final String[] DEFAULT_ALLOWED_EXTENSION = { // 图片 "bmp", "gif", "jpg", "jpeg", "png", // word excel powerpoint "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", // 压缩文件 "rar", "zip", "gz", "bz2", // 视频格式 "mp4", "avi", "rmvb", // pdf "pdf" };...public class FileTypeUtils{ /** * 获取文件类型 *

* 例如: ruoyi.txt, 返回: txt * * @param fileName 文件名 * @return 后缀(不含“”).") */ public static String getFileType(String fileName) { int separatorIndex = fileName.lastIndexOf("."); if (separatorIndex < 0) { return ""; } return fileName.substring(separatorIndex + 1).toLowerCase(); }}

  可见,提交记录 18f6366f 当下载文件时,中间 FileUtils.isValidFilename(fileName) 方法换成了 FileUtils.checkAllowDownload(fileName) 该方法将检查文件名称参数是否包含在此方法中。 .. ,防止跳上目录,然后检查文件名称是否在白名单中。这样可以避免任何文件下载的漏洞。

  允许路径遍历 attack 访问目录和文件的内容通过操作路径的可变部分。在处理上传和下载文件时,我们需要严格检查路径参数,以防止目录的漏洞。三、分页查询排序参数

  RuoYi 作为一个后台管理项目,几乎每个菜单都用于分页查询,所以分页查询类别包装在项目中 PageDomain,其他将读取客户端传入的客户端 orderByColumn 参数。再次提交记录 807b7231 此前,分页查询代码如下: public class PageDomain{ ... public void setOrderByColumn(String orderByColumn) { this.orderByColumn = orderByColumn; } ...}/** * 设置请求分页数据 */public static void startPage(){ PageDomain pageDomain = TableSupport.buildPageRequest(); Integer pageNum = pageDomain.getPageNum(); Integer pageSize = pageDomain.getPageSize(); String orderBy = pageDomain.getOrderBy(); Boolean reasonable = pageDomain.getReasonable(); PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);}/** * 分页查询 */@RequiresPermissions("system:post:list")@PostMapping("/list")@ResponseBodypublic TableDataInfo list(SysPost post){ startPage(); List list = postService.selectPostList(post); return getDataTable(list);}

  可以看出,分页查询通常是直接封装的 startPage() 方法,会将 PageDomain 的 orderByColumn 直接放入属性 PageHelper 最后,它将在实际情况下拼接 SQL 查询句中。3.1 attack 流程

  假如 attack 知道用户名称的人员 users, 构造分页查询请求 传入 orderByColumn 参数为 1; DROP TABLE users; 实际执行的 SQL 可能为:SELECT * FROM users WHERE username = 'admin' ORDER BY 1; DROP TABLE users; 执行 SQL,DROP TABLE users; 完毕,users 表被删除 attack 成功 3.2 如何解决

  再提交记录 807b7231 之后对排序参数进行了转义处理,最新代码如下, public class PageDomain{ ... public void setOrderByColumn(String orderByColumn) { this.orderByColumn = SqlUtil.escapeSql(orderByColumn); }}/** * sql操作工具类 * * @author ruoyi */public class SqlUtil{ /** * 只支持字母、数字、下划线、空间、逗号、小数点(支持多个字段排序) */ public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+"; /** * 检查字符,防止注入绕过 */ public static String escapeOrderBySql(String value) { if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) { throw new UtilException(”参数不符合规范,不能查询”); } return value; } /** * 验证 order by 语法是否符合规范 */ public static boolean isValidOrderBySql(String value) { return value.matches(SQL_PATTERN); } ...}

  可见对 order by 句子结束后,可拼接的字符串进行正则匹配,只支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)。以避免 order by 其他非法字符后面拼接,如 drop|if()|union 等等,可以避免 order by 注入问题。

  SQL 注入是 Web 最常见、最严重的漏洞之一。它允许 attack 将SQL命令插入 Web 在表单提交中实现,非法执行在数据库中 SQL 命令。永远不要相信用户的输入,尤其是在拼接SQL语句时。我们应该过滤用户传输的不可控参数。四、总结

  通过这三个 RuoYi 对于项目中的代码案例,我们可以总结项目开发中应注意的几点: 不要盲目相信用户引入的参数。无论是修改密码还是下载文件,用户引入的参数结构都不应直接使用 SQL 语句或拼接路径会导致 SQL 安全漏洞,如注入和路径遍历。我们应该根据实际业务获得真正的用户 ID 或其它参数,然后操作。 SQL 转换参数。拼接 SQL 在句子中,必须转换用户传入的不可控参数,以防止用户传入的不可控参数 SQL 注入。 检查路径。在处理上传、下载文件等操作时,应检查路径参数,以防止目录中的漏洞。例如,判断路径是否包含在内 .. 字符。 界面应设置权限。对于一些敏感界面,如重置密码,我们需要设置相应的权限,以避免用户越权访问。 记录提交信息。在记录提交信息时,最好详细描述提交的内容,如修复漏洞或新功能。这将有助于后续的代码审计或回顾项目提交的历史。 定期代码审计。作为项目维护人员,我们需要定期进行代码审计,找出项目中可能存在的漏洞,并及时修复。这可以最大限度地保证项目代码的安全性和强度。

  综上所述,编写代码不仅仅是为了完成需求。我们还需要更加关注每个细节,警惕用户传输的参数 SQL 陈述应仔细拼接,并仔细检查路径。定期代码审计可以尽快发现和修复项目漏洞,为用户提供更安全可靠的产品。我希望通过这些案例,我们能提醒您在编写代码的过程中进一步加强安全意识。

  在这篇文章的解释之后,谢谢你的阅读。感兴趣的朋友可以表扬和关注它。你的支持将是我更新的动力