项目讲解之常见安全漏洞
本文是从开源项目RuoYi的提交记录文字描述中根据关键字漏洞安全阻止筛选而来。旨在为大家介绍日常项目开发中需要注意的一些安全问题以及如何解决。项目安全是每个开发人员都需要重点关注的问题。如果项目漏洞太多,很容易遭受黑客攻击与用户信息泄露的风险。本文将结合3个典型案例,解释常见的安全漏洞及修复方案,帮助大家在项目开发中进一步提高安全意识。
RuoYi项目地址:https:gitee。comyprojectRuoYi博主github地址:https:github。comwayn111,欢迎大家关注一、重置用户密码
RuoYi项目中有一个重置用户密码的接口,在提交记录dd37524b之前的代码如下:Log(title重置密码,businessTypeBusinessType。UPDATE)PostMapping(resetPwd)ResponseBodypublicAjaxResultresetPwd(SysUseruser){user。setSalt(ShiroUtils。randomSalt());user。setPassword(passwordService。encryptPassword(user。getLoginName(),user。getPassword(),user。getSalt()));introwsuserService。resetUserPwd(user);if(rows0){setSysUser(userService。selectUserById(user。getUserId()));returnsuccess();}returnerror();}
可以看出该接口会读取传入的用户信息,重置完用户密码后,会根据传入的userId更新数据库以及缓存。
这里有一个非常严重的安全问题就是盲目相信传入的用户信息,如果攻击人员通过接口构造请求,并且在传入的user参数中设置userId为其他用户的userId,那么这个接口就会导致某些用户的密码被重置因而被攻击人员掌握。1。1攻击流程
假如攻击人员掌握了其他用户的userId以及登录账号名构造重置密码请求将userId设置未其他用户的userId服务端根据传入的userId修改用户密码使用新的用户账号以及重置后的密码进行登录攻击成功1。2如何解决
在记录dd37524b提交之后,代码更新如下:Log(title重置密码,businessTypeBusinessType。UPDATE)PostMapping(resetPwd)ResponseBodypublicAjaxResultresetPwd(StringoldPassword,StringnewPassword){SysUserusergetSysUser();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()));returnsuccess();}returnerror();}else{returnerror(修改密码失败,旧密码错误);}}
解决方法其实很简单,不要盲目相信用户传入的参数,通过登录状态获取当前登录用户的userId。如上代码通过getSysUser()方法获取当前登录用户的userId后,再根据userId重置密码。二、文件下载
文件下载作为web开发中,每个项目都会遇到的功能,相信对大家而言都不陌生。RuoYi在提交记录18f6366f之前的下载文件逻辑如下:GetMapping(commondownload)publicvoidfileDownload(StringfileName,Booleandelete,HttpServletResponseresponse,HttpServletRequestrequest){try{if(!FileUtils。isValidFilename(fileName)){thrownewException(StringUtils。format(文件名称({})非法,不允许下载。,fileName));}StringrealFileNameSystem。currentTimeMillis()fileName。substring(fileName。indexOf()1);StringfilePathGlobal。getDownloadPath()fileName;response。setContentType(MediaType。APPLICATIONOCTETSTREAMVALUE);FileUtils。setAttachmentResponseHeader(response,realFileName);FileUtils。writeBytes(filePath,response。getOutputStream());if(delete){FileUtils。deleteFile(filePath);}}catch(Exceptione){log。error(下载文件失败,e);}}publicclassFileUtils{publicstaticStringFILENAMEPATTERN〔azAZ09。u4e00u9fa5〕;publicstaticbooleanisValidFilename(Stringfilename){returnfilename。matches(FILENAMEPATTERN);}}
可以看到代码中在下载文件时,会判断文件名称是否合法,如果不合法会提示文件名称({})非法,不允许下载。的字样。咋一看,好像没什么问题,博主公司项目中下载文件也有这种类似代码。传入下载文件名称,然后再指定目录中找到要下载的文件后,通过流回写给客户端。
既然如此,那我们再看一下提交记录18f6366f的描述信息,
不看不知道,一看吓一跳,原来再这个提交之前,项目中存在任意文件下载漏洞,这里博主给大家讲解一下为什么会存在任意文件下载漏洞。2。1攻击流程
假如下载目录为dataupload构造下载文件请求设置下载文件名称为:。。。。home重要文件。txt服务端将文件名与下载目录进行拼接,获取实际下载文件的完整路径为dataupload。。。。home重要文件。txt由于下载文件包含。。字符,会执行上跳目录的逻辑上跳目录逻辑执行完毕,实际下载文件为home重要文件。txt攻击成功2。2如何解决
我们看一下提交记录18f6366f主要干了什么,代码如下:GetMapping(commondownload)publicvoidfileDownload(StringfileName,Booleandelete,HttpServletResponseresponse,HttpServletRequestrequest){try{if(!FileUtils。checkAllowDownload(fileName)){thrownewException(StringUtils。format(文件名称({})非法,不允许下载。,fileName));}StringrealFileNameSystem。currentTimeMillis()fileName。substring(fileName。indexOf()1);StringfilePathGlobal。getDownloadPath()fileName;response。setContentType(MediaType。APPLICATIONOCTETSTREAMVALUE);FileUtils。setAttachmentResponseHeader(response,realFileName);FileUtils。writeBytes(filePath,response。getOutputStream());if(delete){FileUtils。deleteFile(filePath);}}catch(Exceptione){log。error(下载文件失败,e);}}publicclassFileUtils{检查文件是否可下载paramresource需要下载的文件returntrue正常false非法publicstaticbooleancheckAllowDownload(Stringresource){禁止目录上跳级别if(StringUtils。contains(resource,。。)){returnfalse;}检查允许下载的文件规则if(ArrayUtils。contains(MimeTypeUtils。DEFAULTALLOWEDEXTENSION,FileTypeUtils。getFileType(resource))){returntrue;}不在允许下载的文件规则returnfalse;}}。。。publicstaticfinalString〔〕DEFAULTALLOWEDEXTENSION{图片bmp,gif,jpg,jpeg,png,wordexcelpowerpointdoc,docx,xls,xlsx,ppt,pptx,html,htm,txt,压缩文件rar,zip,gz,bz2,视频格式mp4,avi,rmvb,pdfpdf};。。。publicclassFileTypeUtils{获取文件类型p例如:ruoyi。txt,返回:txtparamfileName文件名return后缀(不含。)publicstaticStringgetFileType(StringfileName){intseparatorIndexfileName。lastIndexOf(。);if(separatorIndex0){return;}returnfileName。substring(separatorIndex1)。toLowerCase();}}
可以看到,提交记录18f6366f中,将下载文件时的FileUtils。isValidFilename(fileName)方法换成了FileUtils。checkAllowDownload(fileName)方法。这个方法会检查文件名称参数中是否包含。。,以防止目录上跳,然后再检查文件名称是否再白名单中。这样就可以避免任意文件下载漏洞。路径遍历允许攻击者通过操纵路径的可变部分访问目录和文件的内容。在处理文件上传、下载等操作时,我们需要对路径参数进行严格校验,防止目录遍历漏洞。
三、分页查询排序参数
RuoYi项目作为一个后台管理项目,几乎每个菜单都会用到分页查询,因此项目中封装了分页查询类PageDomain,其他会读取客户端传入的orderByColumn参数。再提交记录807b7231之前,分页查询代码如下:publicclassPageDomain{。。。publicvoidsetOrderByColumn(StringorderByColumn){this。orderByColumnorderByColumn;}。。。}设置请求分页数据publicstaticvoidstartPage(){PageDomainpageDomainTableSupport。buildPageRequest();IntegerpageNumpageDomain。getPageNum();IntegerpageSizepageDomain。getPageSize();StringorderBypageDomain。getOrderBy();BooleanreasonablepageDomain。getReasonable();PageHelper。startPage(pageNum,pageSize,orderBy)。setReasonable(reasonable);}分页查询RequiresPermissions(system:post:list)PostMapping(list)ResponseBodypublicTableDataInfolist(SysPostpost){startPage();ListSysPostlistpostService。selectPostList(post);returngetDataTable(list);}
可以看到,分页查询一般会直接条用封装好的startPage()方法,会将PageDomain的orderByColumn属性直接放进PageHelper中,最后也就会拼接在实际的SQL查询语句中。3。1攻击流程
假如攻击人员知道用户表名称为users,构造分页查询请求传入orderByColumn参数为1;DROPTABLEusers;实际执行的SQL可能为:SELECTFROMusersWHEREusernameadminORDERBY1;DROPTABLEusers;执行SQL,DROPTABLEusers;完毕,users表被删除攻击成功3。2如何解决
再提交记录807b7231之后,针对排序参数做了转义处理,最新代码如下,publicclassPageDomain{。。。publicvoidsetOrderByColumn(StringorderByColumn){this。orderByColumnSqlUtil。escapeSql(orderByColumn);}}sql操作工具类authorruoyipublicclassSqlUtil{仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)publicstaticStringSQLPATTERN〔azAZ09,。〕;检查字符,防止注入绕过publicstaticStringescapeOrderBySql(Stringvalue){if(StringUtils。isNotEmpty(value)!isValidOrderBySql(value)){thrownewUtilException(参数不符合规范,不能进行查询);}returnvalue;}验证orderby语法是否符合规范publicstaticbooleanisValidOrderBySql(Stringvalue){returnvalue。matches(SQLPATTERN);}。。。}
可以看到对于orderby语句后可以拼接的字符串做了正则匹配,仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)。以此可以避免orderby后面拼接其他非法字符,例如dropif()union等等,因而可以避免orderby注入问题。SQL注入是Web应用中最常见也是最严重的漏洞之一。它允许攻击者通过将SQL命令插入到Web表单提交中实现,数据库中执行非法SQL命令。永远不要信任用户的输入,特别是在拼接SQL语句时。我们应该对用户传入的不可控参数进行过滤。
四、总结
通过这三个RuoYi项目中的代码案例,我们可以总结出项目开发中需要注意的几点:不要盲目相信用户传入的参数。无论是修改密码还是文件下载,都不应该直接使用用户传入的参数构造SQL语句或拼接路径,这会导致SQL注入及路径遍历等安全漏洞。我们应该根据实际业务获取真实的用户ID或其他参数,然后再进行操作。SQL参数要进行转义。在拼接SQL语句时,对用户传入的不可控参数一定要进行转义,防止SQL注入。路径要进行校验。在处理文件上传下载等操作时,对路径参数要进行校验,防止目录遍历漏洞。例如判断路径中是否包含。。字符。接口要设置权限。对一些敏感接口,例如重置密码,我们需要设置对应的权限,避免用户越权访问。记录提交信息。在记录提交信息时,最好详细描述本次提交的内容,例如修复的漏洞或新增的功能。这在后续代码审计或回顾项目提交历史时会很有帮助。定期代码审计。作为项目维护人员,我们需要定期进行代码审计,找出项目中可能存在的漏洞,并及时修复。这可以最大限度地保证项目代码的安全性与健壮性。
综上,写代码不仅仅是完成需求这么简单。我们还需要在各个细节上多加注意,对用户传入的参数要保持警惕,对SQL语句要谨慎拼接,对路径要严谨校验。定期代码审计可以尽早发现并修复项目漏洞,给用户更安全可靠的产品。希望通过这几个案例,可以提醒大家在代码编写过程中进一步加强安全意识。
到此本文讲解完毕,感谢大家阅读,感兴趣的朋友可以点赞加关注,你的支持将是我的更新动力。