上一篇文章,讲述了如何绕过前端文件类型。
详情见:http://793404905.blog.51cto.com/6179428/1566743
1、引言
这一篇讲述一些常见的服务端过滤方式,以及各种过滤方式存在的隐患。并给出怎样处理服务端和前端过滤,以达到更加安全的上传机制。
2、本文大纲
1)Content-Type(Mime Type)检测过滤,以及如何绕过;
2)文件扩展名检测;
3)文件头检测;
4)文件加载检测。
3、Content-Type 检测过滤
按照正常的上传方式,会根据上传的文件类型,指定Content-Type类型,例如:jpg文件对应的Content-Type是image/jpeg;
见下例:
package com.fileupload.servlets; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.List; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; public class UploadFilterExtServlet extends HttpServlet { public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (!ServletFileUpload.isMultipartContent(request)) { response.getWriter().print("NOT MultiPart Request"); return; } String webPath = this.getServletContext().getRealPath(""); DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); factory.setRepository(new File(webPath + File.separator + "tmp")); // 临时仓库 ServletFileUpload fileUpload = new ServletFileUpload(factory); fileUpload.setFileSizeMax(1024 * 1024 * 5); fileUpload.setSizeMax(1024 * 1024 * 6); fileUpload.setHeaderEncoding("utf-8"); try { List<FileItem> fileItems = fileUpload.parseRequest(request); for (FileItem fileItem : fileItems) { String fieldName = fileItem.getFieldName(); // 字段名称 String name = fileItem.getName(); // 如果是表单字段,那么为空;否则为文件名 String contentType = fileItem.getContentType(); // 获取上传文件的Content-Type类型 if (!fileItem.isFormField()) { // 非表单字段,即上传文件 File file = new File(webPath + File.separator + "upImage" + File.separator + name); if (!file.getParentFile().exists()) { file.mkdir(); } if (contentType.equalsIgnoreCase("image/jpeg")) { fileItem.write(file); }else { if (file.exists() && file.isFile()) { fileItem.delete(); response.getWriter().print("Invalid File."); } } } } } catch (FileUploadException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } }
在修改Content-Type之后,服务端将认为此次上传是合法,因此也就绕过了Content-Type的限制。
4、文件扩展名检测
如果在java中使用文件扩展名,并不存在0x00截断的问题,但是如果是asp那么会出现0x00文件截断问题,例如:上传test.txt.jpg 将.修改为0x00,那么系统会认为test.txt才是其文件名称,具体这里不做介绍,但是作为一种相对简单还是有一定效果的检测方式,文件扩展名检测一般是必须的。但是并不代表其是种安全的依靠。
简单而言,我们可以修改文件名以jpg后缀即可,也就可以上传非法文件了。
if (contentType.endsWith("jpg")) { // 将3中代码,判断content-type修改为判断jpg后缀 fileItem.write(file); }
如下图:
那么同样可以上传非图片的文件,可能你会认为上传在服务器上的该文件,已经命名为jpg文件,顶多无法显示,如果你这样想就大错特错,因为可以将非法的脚本嵌入到文件中。并且文件名扩展的检测一般使用白名单比较好,因为黑名单难免会有遗漏,一旦遗漏了,也可能会有致命的问题。
5、文件头检测
通常一个文件会有一种标识,即表明该文件的类型。因此采用4中的方式上传一个txt文件,虽然其绕过了后缀名的检测,但是此时我们可以对该文件进行检测,初步判定该文件是否是jpg文件,也就是通过文件头来判定。
文件头一般是一个文件的开头字节内容,如下代码,展示java获取文件头的方式:
package com.fileupload.types; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.apache.commons.codec.binary.Hex; /** * 判断文件头类型是否合法 * @author wangzp * */ public class FileType { public final static String JPG = "FFD8FF"; public final static String PNG = "89504E47"; public final static String GIF = "47494638"; public final static String BMP = "424D"; public final static Map<String, String> fileTypes = new HashMap<String, String>(); static { fileTypes.put("jpg", JPG); fileTypes.put("png", PNG); fileTypes.put("gif", GIF); fileTypes.put("bmp", BMP); } /** * 获取文件头 * @param filepath * @return * @throws IOException */ public static String getFileHeader(File file) throws IOException { FileInputStream input = new FileInputStream(file); byte[] buffer = new byte[4]; input.read(buffer, 0, buffer.length); input.close(); return new String(Hex.encodeHex(buffer)); } /** * 验证文件头类型是否合法 * @param fileType * @param file * @return * @throws IOException */ public static boolean isValidFile(String fileType, File file) throws IOException { String fileHeader = getFileHeader(file); String fileTypeHeader = fileTypes.get(fileType); if (fileHeader == null || fileTypeHeader == null) { return false; } if (fileHeader.startsWith(fileTypeHeader)) { return true; } return false; } }
由此,我们可以在上传之后判断该文件是否是合法文件,如下代码展示:
if (name.endsWith("jpg")) { fileItem.write(file); if (!FileType.isValidFile("jpg", file)) { // fileItem.delete(); file.delete(); return; }else { response.getWriter().print("Invalid File Header."); return; } }
代码有点粗糙,但基本可以展示出使用test.txt伪装的jpg文件,是无法上传成功的;但是这不是绝对的,因此可以在图片中加入虚假的文件头。那么面对这种情况,该如何解决呢?接下来将使用文件加载检测。
6、文件加载检测
文件加载实际上是对文件的预览方式,可以分为一次渲染和二次渲染;一般而言二次渲染后的图片很难攻入,很难在图片中嵌入代码,因此个人建议使用二次渲染,至少应该一次渲染,如果渲染失败,可以认为该文件是非法文件。不让其上传。
在本文中,不介绍文件加载检测过程,将在后续文章中介绍图片渲染的方法。