項目講解之常見安全漏洞

来源: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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...