共计 7376 个字符,预计需要花费 19 分钟才能阅读完成。
本文是从开源我的项目 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" }; | |
... | |
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 语句要审慎拼接, 对门路要谨严校验。定期代码审计能够尽早发现并修复我的项目破绽,给用户更安全可靠的产品。心愿通过这几个案例,能够揭示大家在代码编写过程中进一步增强安全意识。
到此本文解说结束,感激大家浏览,感兴趣的敌人能够点赞加关注,你的反对将是我的更新能源😘。
公众号【waynblog】每周更新博主最新技术文章,欢送大家关注