项目讲解之常见安全漏洞

这篇具有很好参考价值的文章主要介绍了项目讲解之常见安全漏洞。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

  • RuoYi项目地址:https://gitee.com/y_project/RuoYi
  • 博主github地址:https://github.com/wayn111,欢迎大家关注

一、重置用户密码

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.toymoban.com/news/detail-417816.html

到了这里,关于项目讲解之常见安全漏洞的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 常见的安全扫描漏洞的工具、漏洞分类及处理

    Nikto 这是一个开源的Web服务器扫描程序,它可以对Web服务器的多种项目进行全面的测试。其扫描项目和插件经常更新并且可以自动更新。Nikto可以在尽可能短的周期内测试你的Web服务器,这在其日志文件中相当明显。不过,如果你想试验一下,它也可以支持 LibWhisker的反IDS方法

    2023年04月08日
    浏览(53)
  • 【XXE漏洞安全-全网最详细讲解】

    作者: liku君 公众号:里库的安全日记 博主微信:L2440710476(请备注来意) XXE(XML External Entity)漏洞是一种安全漏洞,出现在使用XML解析器的应用程序中。它允许攻击者利用可信任的XML扩展功能来执行恶意操作,如读取本地文件、发起远程网络请求或执行任意命令。 XXE漏洞

    2024年04月17日
    浏览(44)
  • 常见的安全测试漏洞

    SQL 注入攻击主要是由于程序员在开发过程中没有对客户端所传输到服务器端的参数进行严格的安全检查,同时 SQL 语句的执行引用了该参数,并且 SQL 语句采用字符串拼接的方式执行时,攻击者将可能在参数中插入恶意的 SQL 查询语句,导致服务器执行了该恶意 SQL 语句。 SQ

    2024年02月08日
    浏览(45)
  • Solidity 合约安全,常见漏洞 (下篇)

    Solidity 合约安全,常见漏洞 (上篇) 目前不可能用区块链上的单一交易安全地产生随机数。区块链需要是完全确定的,否则分布式节点将无法达成关于状态的共识。因为它们是完全确定的,所以任何 \\\"随机\\\"的数字都可以被预测到。下面的掷骰子函数可以被利用。 如何来产生

    2024年02月11日
    浏览(50)
  • [Java安全]—weblogic常见漏洞

    本来想跟一些T3反序列化的,奈何本地环境怎么都起不起来先复现一下常见漏洞吧。。。 Weblogic是美国Oracle公司出品的一个应用服务器(application server),确切的说是一个基于Java EE架构的中间件,是用于开发、集成、部署和管理大型分布式Web应用、网络应用和 数据库应用的Jav

    2024年02月06日
    浏览(38)
  • 中间件安全—Apache常见漏洞

      简单介绍一下apache是什么,Apache是世界使用排名第一的Web服务器软件。它可以运行在几乎所有广泛使用的计算机上,由于其跨平台和安全性被广泛使用,是最流行的Web服务器端软件之一。它快速、可靠并且可通过简单的API扩充,将python等解释器编译到服务器中。 1.2.1.漏洞

    2024年01月22日
    浏览(52)
  • 中间件安全—Nginx常见漏洞

      在上篇中间件安全—Apache常见漏洞中,并未对中间件漏洞进行解释,这里补充一下。   所谓的中间件漏洞就是并非是由于代码程序上设计存在缺陷而导致的漏洞,而是属于应用部署中环境配置不当或使用不当而导致的漏洞,同时这方面的漏洞也是最容易被管理员忽略的

    2024年02月09日
    浏览(46)
  • 中间件安全—Tomcat常见漏洞

      整个复现环境均使用Vulhub一键搭建漏洞测试靶场,Vulhub靶场:链接 1.2.1.漏洞原理   Tomcat配置文件/conf/web.xml 配置了可写(readonly=false),导致可以使用PUT方法上传任意文件,攻击者将精心构造的payload向服务器上传包含任意代码的 JSP 文件。之后,JSP 文件中的代码将能被

    2024年02月08日
    浏览(60)
  • Solidity 合约安全,常见漏洞(第三篇)

    如果你只处理受信任的 ERC20 代币,这些问题大多不适用。然而,当与任意的或部分不受信任的 ERC20 代币交互时,就有一些需要注意的地方。 ERC20:转账扣费 当与不信任的代币打交道时,你不应该认为你的余额一定会增加那么多。一个 ERC20 代币有可能这样实现它的转账函数,

    2024年02月09日
    浏览(41)
  • 网络&信息安全:11个常见漏洞类型汇总

    SQL注入攻击(SQL Injection),简称注入攻击、SQL注入,被广泛用于非法获取网站控制权,是发生在应用程序的数据库层上的安全漏洞。 在设计程序,忽略了对输入字符串中夹带的SQL指令的检查,被数据库误认为是正常的SQL指令而运行,从而使数据库受到攻击,可能导致数据被

    2024年03月12日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包