项目讲解之常见安全漏洞

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

项目安全是每个开发人员都需要重点关注的问题。如果项目漏洞太多,很容易遭受黑客攻击与用户信息泄露的风险。本文将结合3个典型案例,解释常见的安全漏洞及修复方案,帮助大家在项目开发中进一步提高安全意识。

一、重置用户密码

RuoYi 项目中有一个重置用户密码的接口,在提交记录 dd37524b 之前的代码如下:

@Log(title = "重置密码", businessType = BusinessType.UPDATE)
@PostMapping("/resetPwd")
@ResponseBody
public 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 更新数据库以及缓存。

这里有一个非常严重的安全问题就是盲目相信传入的用户信息,如果攻击人员通过接口构造请求,并且在传入的 user 参数中设置 userId 为其他用户的 userId,那么这个接口就会导致某些用户的密码被重置因而被攻击人员掌握。

1.1 攻击流程

假如攻击人员掌握了其他用户的 userId 以及登录账号名

  1. 构造重置密码请求
  2. 将 userId 设置未其他用户的 userId
  3. 服务端根据传入的 userId 修改用户密码
  4. 使用新的用户账号以及重置后的密码进行登录
  5. 攻击成功

1.2 如何解决

在记录 dd37524b 提交之后,代码更新如下:

@Log(title = "重置密码", businessType = BusinessType.UPDATE)
@PostMapping("/resetPwd")
@ResponseBody
public 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 攻击流程

假如下载目录为 /data/upload/

  1. 构造下载文件请求
  2. 设置下载文件名称为:../../home/重要文件.txt
  3. 服务端将文件名与下载目录进行拼接,获取实际下载文件的完整路径为 /data/upload/../../home/重要文件.txt
  4. 由于下载文件包含 .. 字符,会执行上跳目录的逻辑
  5. 上跳目录逻辑执行完毕,实际下载文件为 /home/重要文件.txt
  6. 攻击成功

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
{
    /**
     * 获取文件类型
     * <p>
     * 例如: 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) 方法。这个方法会检查文件名称参数中是否包含 .. ,以防止目录上跳,然后再检查文件名称是否再白名单中。这样就可以避免任意文件下载漏洞。

路径遍历允许攻击者通过操纵路径的可变部分访问目录和文件的内容。在处理文件上传、下载等操作时,我们需要对路径参数进行严格校验,防止目录遍历漏洞。

三、分页查询排序参数

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")
@ResponseBody
public TableDataInfo list(SysPost post)
{
    startPage();
    List<SysPost> list = postService.selectPostList(post);
    return getDataTable(list);
}

可以看到,分页查询一般会直接条用封装好的 startPage() 方法,会将 PageDomainorderByColumn 属性直接放进 PageHelper 中,最后也就会拼接在实际的 SQL 查询语句中。

3.1 攻击流程

假如攻击人员知道用户表名称为 users,

  1. 构造分页查询请求
  2. 传入 orderByColumn 参数为 1; DROP TABLE users;
  3. 实际执行的 SQL 可能为:SELECT * FROM users WHERE username = 'admin' ORDER BY 1; DROP TABLE users;
  4. 执行 SQL,DROP TABLE users; 完毕,users 表被删除
  5. 攻击成功

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 应用中最常见也是最严重的漏洞之一。它允许攻击者通过将SQL命令插入到 Web 表单提交中实现,数据库中执行非法 SQL 命令。
永远不要信任用户的输入,特别是在拼接SQL语句时。我们应该对用户传入的不可控参数进行过滤。

四、总结

通过这三个 RuoYi 项目中的代码案例,我们可以总结出项目开发中需要注意的几点:

  1. 不要盲目相信用户传入的参数。无论是修改密码还是文件下载,都不应该直接使用用户传入的参数构造 SQL 语句或拼接路径,这会导致 SQL 注入及路径遍历等安全漏洞。我们应该根据实际业务获取真实的用户 ID 或其他参数,然后再进行操作。
  2. SQL 参数要进行转义。在拼接 SQL 语句时,对用户传入的不可控参数一定要进行转义,防止 SQL 注入。
  3. 路径要进行校验。在处理文件上传下载等操作时,对路径参数要进行校验,防止目录遍历漏洞。例如判断路径中是否包含 .. 字符。
  4. 接口要设置权限。对一些敏感接口,例如重置密码,我们需要设置对应的权限,避免用户越权访问。
  5. 记录提交信息。在记录提交信息时,最好详细描述本次提交的内容,例如修复的漏洞或新增的功能。这在后续代码审计或回顾项目提交历史时会很有帮助。
  6. 定期代码审计。作为项目维护人员,我们需要定期进行代码审计,找出项目中可能存在的漏洞,并及时修复。这可以最大限度地保证项目代码的安全性与健壮性。

综上,写代码不仅仅是完成需求这么简单。我们还需要在各个细节上多加注意,对用户传入的参数要保持警惕,对 SQL 语句要谨慎拼接,对路径要严谨校验。定期代码审计可以尽早发现并修复项目漏洞,给用户更安全可靠的产品。希望通过这几个案例,可以提醒大家在代码编写过程中进一步加强安全意识。

到此本文讲解完毕,感谢大家阅读,感兴趣的朋友可以点赞加关注,你的支持将是我的更新动力?。

公众号【waynblog】每周更新博主最新技术文章,欢迎大家关注

原文链接:https://www.cnblogs.com/waynaqua/p/17331595.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:项目讲解之常见安全漏洞 - Python技术站

(0)
上一篇 2023年4月19日
下一篇 2023年4月19日

相关文章

  • MATLAB算法技巧和实现斐波那契数列的解决思路

    MATLAB算法技巧和实现斐波那契数列的解决思路 概述 MATLAB是一款广泛使用的科学计算软件,其中涉及的算法技巧和实现方法是使用MATLAB进行数据分析和模拟的重点内容之一。这篇文章主要介绍MATLAB中应用于算法优化和实现的技巧和思路,并且使用其中的技巧和思路实现斐波那契数列。 MATLAB算法优化技巧 MATLAB中涉及的算法可以通过一些技巧和方法进…

    Java 2023年5月19日
    00
  • Java SpringBoot实现文件上传功能的示例代码

    下面是Java SpringBoot实现文件上传功能的详细攻略: 1. 项目依赖 首先需要在SpringBoot项目中引入以下依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-…

    Java 2023年5月19日
    00
  • Springmvc处理ajax请求并返回json数据

    下面我将介绍SpringMVC处理ajax请求并返回JSON数据的完整攻略。 什么是SpringMVC SpringMVC是一个基于Spring框架之上的Web框架,它可以帮助我们简化Web应用程序的开发,并且具有良好的可扩展性和灵活性。SpringMVC中最常见的请求方式是通过URL来映射到处理器(Controller)中的某个具体的方法,并由该方法来处理…

    Java 2023年6月15日
    00
  • JavaSpringBoot报错“HttpMessageNotReadableException”的原因和处理方法

    原因 “HttpMessageNotReadableException” 错误通常是以下原因引起的: 请求体不正确:如果请求体不正确,则可能会出现此错误。在这种情况下,您需要检查请求体并确保它们正确。 请求体格式不正确:如果请求体格式不正确,则可能会出现此错误。在这种情况下,您需要检查请求体格式并确保它们正确。 解决办法 以下是解决 “HttpMessage…

    Java 2023年5月4日
    00
  • 转载一个别人收藏的精典网站Ruby,HIBERNATE相关

    关于“转载一个别人收藏的精典网站Ruby,HIBERNATE相关”的完整攻略,我会按照以下步骤进行详细讲解: 1. 确定转载目的 在转载一篇文章之前,我们需要明确自己的转载目的。是为了丰富自己的博客内容,还是为了分享给更多人?这一点很重要,因为它将决定你应该如何进行转载。 2. 征求原作者许可 在转载别人的文章之前,最重要的是要获得原作者的授权,否则可能会引…

    Java 2023年5月20日
    00
  • Java实现UTF-8编码与解码方式

    我会为你详细讲解如何用Java实现UTF-8编码与解码。首先,让我们了解一下UTF-8编码的相关知识。 UTF-8是一种可变长度的Unicode编码,它能够表示Unicode标准中的任何字符。UTF-8编码使用1到4个字节来表示每个字符,其中ASCII字符只需要使用1个字节。 在Java中,可以使用java.nio.charset.Charset类来支持UT…

    Java 2023年5月20日
    00
  • Java中常用解析工具jackson及fastjson的使用

    Java中常用解析工具jackson及fastjson的使用攻略 jackson 1. 简介 jackson是一种可以将java对象转换为JSON格式,也可以将JSON格式转换为java对象的工具。它为一个高性能的JSON处理库,是Spring框架的默认JSON格式解析工具,此外也逐渐成为Java领域内最流行的JSON解析器之一。 2. 快速开始 首先我们需…

    Java 2023年5月26日
    00
  • MyBatis无缝对接Spring的方法

    MyBatis是Java中使用最广泛的ORM框架之一。该框架提供了简单易用的映射工具,可以帮助我们轻松实现实体类到数据库表之间的映射。同时,Spring是一种非常流行的Java开发框架,可以提供依赖注入、AOP等功能,使得Java应用变得更加易于开发和维护。这里我们将介绍如何将MyBatis与Spring框架结合使用,以便更好地开发Web应用。 以下是MyB…

    Java 2023年5月20日
    00
合作推广
合作推广
分享本页
返回顶部