今天的这篇文章来自蜗牛学院重庆校区刘颖聪老师。
蜗牛学院资深导师,重庆邮电大学计算机专业学士学位,12年软件开发、管理及培训经验。精通开发和逆向编译技术。曾在天收网络技术有限公司担任逆向编译工程师,腾讯,盛大驱动保护项目合作。叁壹伍捌网络文化公司研发部项目经理,参与站群建设,SEO,反黑技术等工作。国内某IT培训机构资深项目经理,教学幽默风趣,注重案例与生活的结合,着重培养学员独立解决问题的思想及技术层次的扩展。
· 正 · 文 · 来 · 啦 ·
对于开发Javaweb项目来说,经常会有让用户通过浏览器上传文件的需求,比如上传用户头像(jpg格式)。那么像这样的功能我们通常的实现方式都是在前端页面先验证文件的后缀名是否是jpg格式,通过验证之后,再进行上传请求操作,接下来服务器端接受文件之后,再一次验证文件后缀名是否jpg,之后再讲文件保存到指定文件夹。类似代码如下所示:
前端:
<script type="text/javascript"src="js/jquery-1.11.3.js"></script> <script type="text/javascript"> // 上传文件 function uploadFunc(){ var file = $("#file")[0].files[0]; var fileName = file.name; var suffixName = fileName.substring(fileName.lastIndexOf(".")); if( suffixName != ".jpg" ){ $("#info").html("请上传jpg格式图片!"); return false; } var fileData = new FormData(); fileData.append("file", file); fileData.append("fileName", fileName); $.ajax({ url:"uploadsuffix", type:"post", data:fileData, processData:false, contentType:false, success:function(rt) { $("#info").html( rt.msg ); } }); return false; } </script> |
后端:
@MultipartConfig @WebServlet("/uploadsuffix") public class UploadSuffix extends HttpServlet{ @Override protected void service(HttpServletRequestreq, HttpServletResponseresp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); resp.setContentType("application/json;charset=utf-8"); JSONObjectjson = new JSONObject(); // 获取文件 Part part = req.getPart("file"); String fileName = req.getParameter("fileName"); String suffixName = fileName.substring(fileName.lastIndexOf(".")); // 文件后缀验证 if( ".jpg".equals(suffixName) ){ // 保存路径 String clsPath = this.getClass().getResource("/").getPath(); String contextPath = req.getContextPath(); String filePath = clsPath.substring(0, clsPath.lastIndexOf(contextPath)) + contextPath + "/upload"; InputStream in = part.getInputStream(); OutputStream out = new FileOutputStream(new File( filePath + "/" + fileName)); byte[]byts = new byte[1024]; int len = -1; while( (len = in.read(byts)) != -1 ){ out.write(byts, 0,len); } in.close(); out.close(); json.put("msg", "上传图片成功!"); }else{ json.put("msg", "请上传jpg格式图片!"); } PrintWriter print = resp.getWriter(); print.println( JSONObject.fromObject(json) ); print.close(); return; } } |
上面的常规做法的确可以验证做到对文件格式的验证,但却存在一个很大的漏洞。那就是如果用户将一个xxx.txt文件的后缀名改为xxx.jpg文件,同样也可以通过前后端的验证并且上传成功。而当我们需要用户上传头像这种功能的时候,前端通常也会将用户上传头像图片展示出来。这个时候用户上传的图片文件会就会被我们的前端代码执行比如<img src=”xxx.jpg”>。
如果别有用心的用户将一个xxx.js文件改名问xxx.jpg文件,那么我们的程序将随着用户上传的文件被强行注入一个js,造成注入式攻击漏洞。
相关流程如下图所示:
应该怎么去解决以上的漏洞问题呢?很明显以前单一的文件后缀验证已经无法满足了。那么我们需要转换一下验证思路。
我们都知道,不同的网络协议有对应不同的消息头。同样的,不同类型的文件类型在操作系统中被鉴别出来也需要对应的文件头。我们用UltraEdit将jpg文件打开,以十六进制内存形式表示如下图:
我们可以看到前4个字节的值为:FFD8DDE0。同样的方式打开其他的.jpg文件它的头文件前4个字节的值也相同。而换一个.png文件以同样的方式打开,前4字节为:89504E47。如下图所示:
综上所述我们可以用Java将上传图片文件头信息解析出来,和正常jpg文件头前4字节做一个对比的方式来判断文件文件类型。前端代码不变,后端代码稍作修改如下所示:
private static final Integer FILE_HEAD_LEN = 4; private static final String JPG_HEAD = "FFD8FFE0";
@Override protected void service(HttpServletRequestreq, HttpServletResponseresp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); resp.setContentType("application/json;charset=utf-8"); JSONObjectjson = new JSONObject(); Stringmsg = ""; // 获取文件 Part part = req.getPart("file"); String fileName = req.getParameter("fileName"); InputStream in = part.getInputStream(); // 检查文件头 byte[]byts = new byte[ FILE_HEAD_LEN ]; in.read(byts, 0, FILE_HEAD_LEN); StringBuilder strB = new StringBuilder(); for (int i = 0; i < byts.length; i++) { // 组装补码 strB.append(Integer.toHexString(byts[i] & 0xff ).toUpperCase()); } if( JPG_HEAD.equals(strB.toString()) ){// 合法文件 String clsPath = this.getClass().getResource("/").getPath(); String contextPath = req.getContextPath(); String filePath = clsPath.substring(0, clsPath.lastIndexOf(contextPath)) + contextPath + "/upload"; OutputStream out = new FileOutputStream(new File( filePath + "/" + fileName)); in.skip(-FILE_HEAD_LEN); byts = new byte[ 1024 ]; int len = -1; while( (len = in.read(byts)) != -1 ){ out.write(byts, 0,len); } out.close(); msg = "上传图片成功!"; }else{// 非法文件 msg = "请上传jpg格式图片!"; } in.close(); json.put("msg",msg); PrintWriter print = resp.getWriter(); print.println( JSONObject.fromObject(json) ); print.close(); return; } |
以上,我们通过判断文件头的形式可以更好的解决单一的通过后缀名判断上传文件留下来的bug问题。上述代码中需要注意的是byts[i] & 0xff这一句,因为在计算机内的数据储存都是以二进制的补码进行存储的,所以用位与的方式可以更快速的获得计算效率。
然后还有两点是需要我们注意的:(代码就大同小异就不再演示了)
1. 如果上传的是大文件如mp4,rmvb等,可以先将文件头信息上传到服务器端,通过验证之后再传剩下的部分。
2. 如果上传的文件是office系列,如docx或xlsx等。这一类行的文件头比较大,只比较前4个字节是无法区分的。应该增加头文件获取的长度。
最后分享几组常见图片格式的头文件信息:
jpg:FFD8FFE0
png:89504E47
gif:47494638
bmp:424D4E9B