本文是從開源項目 RuoYi 的提交記錄文字描述中根據關鍵字漏洞|安全|阻止篩選而來。旨在為大家介紹日常項目開發中需要註意的一些安全問題以及如何解決。 項目安全是每個開發人員都需要重點關註的問題。如果項目漏洞太多,很容易遭受黑客攻擊與用戶信息泄露的風險。本文將結合3個典型案例,解釋常見的安全漏洞及修 ...
本文是從開源項目 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 以及登錄賬號名
- 構造重置密碼請求
- 將 userId 設置未其他用戶的 userId
- 服務端根據傳入的 userId 修改用戶密碼
- 使用新的用戶賬號以及重置後的密碼進行登錄
- 攻擊成功
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/
- 構造下載文件請求
- 設置下載文件名稱為:
../../home/重要文件.txt
- 服務端將文件名與下載目錄進行拼接,獲取實際下載文件的完整路徑為
/data/upload/../../home/重要文件.txt
- 由於下載文件包含 .. 字元,會執行上跳目錄的邏輯
- 上跳目錄邏輯執行完畢,實際下載文件為
/home/重要文件.txt
- 攻擊成功
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()
方法,會將 PageDomain
的 orderByColumn
屬性直接放進 PageHelper
中,最後也就會拼接在實際的 SQL 查詢語句中。
3.1 攻擊流程
假如攻擊人員知道用戶表名稱為 users,
- 構造分頁查詢請求
- 傳入
orderByColumn
參數為1; DROP TABLE users;
- 實際執行的 SQL 可能為:
SELECT * FROM users WHERE username = 'admin' ORDER BY 1; DROP TABLE users;
- 執行 SQL,
DROP TABLE users;
完畢,users 表被刪除 - 攻擊成功
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 項目中的代碼案例,我們可以總結出項目開發中需要註意的幾點:
- 不要盲目相信用戶傳入的參數。無論是修改密碼還是文件下載,都不應該直接使用用戶傳入的參數構造 SQL 語句或拼接路徑,這會導致 SQL 註入及路徑遍歷等安全漏洞。我們應該根據實際業務獲取真實的用戶 ID 或其他參數,然後再進行操作。
- SQL 參數要進行轉義。在拼接 SQL 語句時,對用戶傳入的不可控參數一定要進行轉義,防止 SQL 註入。
- 路徑要進行校驗。在處理文件上傳下載等操作時,對路徑參數要進行校驗,防止目錄遍歷漏洞。例如判斷路徑中是否包含 .. 字元。
- 介面要設置許可權。對一些敏感介面,例如重置密碼,我們需要設置對應的許可權,避免用戶越權訪問。
- 記錄提交信息。在記錄提交信息時,最好詳細描述本次提交的內容,例如修複的漏洞或新增的功能。這在後續代碼審計或回顧項目提交歷史時會很有幫助。
- 定期代碼審計。作為項目維護人員,我們需要定期進行代碼審計,找出項目中可能存在的漏洞,並及時修複。這可以最大限度地保證項目代碼的安全性與健壯性。
綜上,寫代碼不僅僅是完成需求這麼簡單。我們還需要在各個細節上多加註意,對用戶傳入的參數要保持警惕,對 SQL 語句要謹慎拼接,對路徑要嚴謹校驗。定期代碼審計可以儘早發現並修複項目漏洞,給用戶更安全可靠的產品。希望通過這幾個案例,可以提醒大家在代碼編寫過程中進一步加強安全意識。
到此本文講解完畢,感謝大家閱讀,感興趣的朋友可以點贊加關註,你的支持將是我的更新動力