項目講解之常見安全漏洞

来源:https://www.cnblogs.com/waynaqua/archive/2023/04/18/17331595.html
-Advertisement-
Play Games

本文是從開源項目 RuoYi 的提交記錄文字描述中根據關鍵字漏洞|安全|阻止篩選而來。旨在為大家介紹日常項目開發中需要註意的一些安全問題以及如何解決。 項目安全是每個開發人員都需要重點關註的問題。如果項目漏洞太多,很容易遭受黑客攻擊與用戶信息泄露的風險。本文將結合3個典型案例,解釋常見的安全漏洞及修 ...


本文是從開源項目 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 語句要謹慎拼接,對路徑要嚴謹校驗。定期代碼審計可以儘早發現並修複項目漏洞,給用戶更安全可靠的產品。希望通過這幾個案例,可以提醒大家在代碼編寫過程中進一步加強安全意識。

到此本文講解完畢,感謝大家閱讀,感興趣的朋友可以點贊加關註,你的支持將是我的更新動力

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 後臺管理系統在實際開發中,表格如果在一定高度出現滾動條。 這時如果對錶格行數據進行編輯或者拖拽排序操作,數據刷新後滾動條會預設回到頂部,這樣體驗會不太好。 如果想保留在當前位置可以這樣操作: 1.el-table標簽添加ref屬性 <el-table :data="tableData" v-load ...
  • #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實例。 // 其他有用的實例變數寫在這裡 //構造器聲明為私有,只有Singleton可以實例化這個類! ...
  • 軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,並遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ...
  • HOOPS Communicator在2021版本中,推出了基於PBR(Physically Based Rendering)的渲染特性以提供更高質量的渲染技術。 PBR將材料表示為一系列方程,這些方程對光如何從錶面反射進行建模,再通過GPU上運行的著色器代碼進行有效地實現。 一、工程領域可視化問題 ...
  • L2-3 智能護理中心統計 智能護理中心系統將轄下的護理點分屬若幹個大區,例如華東區、華北區等;每個大區又分若幹個省來進行管理;省又分市,等等。我們將所有這些有管理或護理功能的單位稱為“管理結點”。現在已知每位老人由唯一的一個管理結點負責,每個管理結點屬於唯一的上級管理結點管轄。你需要實現一個功能, ...
  • JSP全名為Java Server Pages,java伺服器頁面。JSP是一種基於文本的程式,其特點就是HTML和Java代碼共同存在!JSP是為了簡化Servlet的工作出現的替代品,Servlet輸出HTML非常困難,JSP就是替代Servlet輸出HTML的。JSP本身就是一種Servlet ...
  • 本文主要使用目前較新版本elastic search 8.5.0 + kibna 8.5.0 + springboot 3.0.2 + spring data elasticsearch 5.0.2 + jdk 17 進行搜索功能的開發。 ...
  • Auto-GPT嘗鮮使用 註:部署所需:OpenAI的API Key 1. Auto-GPT本地部署 1.1. 環境準備 需要Python環境,Python版本建議>=3.8(官方寫的>=3.10) 建議用Conda(Minconda或Anaconda)創建單獨的虛擬環境 Git:有沒有無所謂了 1 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...