本文是從開源專案 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,那麼這個介面就會導致某些使用者的密碼被重置因而被攻擊人員掌握。
假如攻擊人員掌握了其他使用者的 userId 以及登入賬號名
在記錄 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
的描述資訊,
不看不知道,一看嚇一跳,原來再這個提交之前,專案中存在任意檔案下載漏洞,這裡博主給大家講解一下為什麼會存在任意檔案下載漏洞。
假如下載目錄為 /data/upload/
../../home/重要檔案.txt
/data/upload/../../home/重要檔案.txt
/home/重要檔案.txt
我們看一下提交記錄 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 查詢語句中。
假如攻擊人員知道使用者表名稱為 users,
orderByColumn
引數為 1; DROP TABLE users;
SELECT * FROM users WHERE username = 'admin' ORDER BY 1; DROP TABLE users;
DROP TABLE users;
完畢,users 表被刪除再提交記錄 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 語句要謹慎拼接,對路徑要嚴謹校驗。定期程式碼審計可以儘早發現並修復專案漏洞,給使用者更安全可靠的產品。希望通過這幾個案例,可以提醒大家在程式碼編寫過程中進一步加強安全意識。
到此本文講解完畢,感謝大家閱讀,感興趣的朋友可以點贊加關注,你的支援將是我的更新動力