项目讲解之常见安全漏洞

本文是从开源项目 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日

相关文章

  • Java函数式编程(三):列表的转化

    Java函数式编程(三):列表的转化指的是如何使用函数式编程的思想来对列表进行转化操作。常见的列表转化操作有过滤、映射、归约等。下面是本文的完整攻略。 1. 列表的创建 在进行列表转化之前,我们首先需要了解如何创建一个Java列表。Java中列表的创建可以使用Java集合框架中的ArrayList类。可以通过以下方法进行创建: List<Integer…

    Java 2023年5月26日
    00
  • java实现客户管理系统

    Java实现客户管理系统完整攻略 1. 目标 本文将详细介绍Java实现客户管理系统的完整攻略,包含以下内容: 需求分析和设计方案 前端页面设计和开发 数据库设计和操作 后端Java代码实现 测试和部署 2. 需求分析和设计方案 2.1 需求分析 客户管理系统是一种管理客户信息的应用程序,通常主要包括以下功能: 客户信息的录入和修改 客户信息的删除和查询 客…

    Java 2023年5月19日
    00
  • Spring Boot2开发之Spring Boot整合Shiro两种详细方法

    SpringBoot2开发之SpringBoot整合Shiro两种详细方法 Shiro是一个强大且易于使用的Java安全框架,可以提供身份验证、授权、加密和会话管理等功能。本文将详细讲解如何在Spring Boot应用程序中整合Shiro,包括两种详细方法。 方法一:使用Shiro Spring Boot Starter Shiro Spring Boot …

    Java 2023年5月15日
    00
  • Java实现统计文档中关键字出现的次数

    为了实现统计文档中关键字出现的次数,我们需要以下步骤: 步骤一:获取文档内容 首先,我们需要读取文档中的内容,可以使用Java中的File类来进行文件的读取,示例代码如下: File file = new File("example.txt"); try { BufferedReader br = new BufferedReader(n…

    Java 2023年5月29日
    00
  • 带你入门Java的类与对象

    接下来我将向您介绍如何入门Java的类与对象。 1. 什么是类与对象 在Java中,我们可以使用类来定义一个对象。类是指一种自定义数据类型,可以包含数据和行为。对象是类的实例,是具体存在的,可以使用这个对象来调用类中定义的方法。 2. 定义类与对象 先来看一个简单的示例,定义一个类Person,包含属性name和age,构造方法和一个方法sayHello。 …

    Java 2023年5月19日
    00
  • 教你怎么用Java操作Redis

    下面为你详细讲解如何使用Java操作Redis: 1. 前置条件 在使用Java操作Redis之前,你需要确保已经完成以下步骤: 在本地或者服务器上安装Redis并启动。 在Java项目中引入Redis的Java客户端依赖包,例如Jedis或Lettuce。 2. 连接Redis 在Java中连接Redis的方式很简单,只需要创建一个Redis客户端对象,设…

    Java 2023年5月26日
    00
  • SpringBoot RESTful 应用中的异常处理梳理小结

    SpringBoot RESTful 应用中的异常处理是非常重要的一部分。异常处理可以让我们及时的判断和处理错误,保障服务的稳定性和可靠性。在这里,我将为您提供关于 SpringBoot RESTful 应用中异常处理的梳理和完整攻略。 一、异常处理的基本架构 异常处理的基本架构分为两个部分:异常拦截器和异常处理器。拦截器负责拦截异常,而处理器则负责对异常进…

    Java 2023年5月27日
    00
  • spring 和 spring boot 中的属性配置方式

    Spring和Spring Boot中的属性配置方式 Spring和Spring Boot都提供了多种属性配置方式,本文将详细介绍这些方式,并提供两个示例。 Spring中的属性配置方式 Spring中的属性配置方式有以下几种: 1. 使用XML配置文件 使用XML配置文件是Spring最早的属性配置方式。在XML配置文件中,我们可以使用元素来定义Bean,…

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