上一篇文章,讲述了如何绕过前端文件类型。

    详情见: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是p_w_picpath/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("p_w_picpath/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();
		}
		
	}

}

    wKioL1RZ4NjSEeJlAACFMekC-6E923.jpg

    在修改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);
}

    如下图:

wKiom1RZ4l2BMCc7AACIQ2gos_E045.jpg

    那么同样可以上传非图片的文件,可能你会认为上传在服务器上的该文件,已经命名为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、文件加载检测

    文件加载实际上是对文件的预览方式,可以分为一次渲染和二次渲染;一般而言二次渲染后的图片很难攻入,很难在图片中嵌入代码,因此个人建议使用二次渲染,至少应该一次渲染,如果渲染失败,可以认为该文件是非法文件。不让其上传。

    在本文中,不介绍文件加载检测过程,将在后续文章中介绍图片渲染的方法。