上一篇文章,讲述了如何绕过前端文件类型。
详情见: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();
}
}
}
在修改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、文件加载检测
文件加载实际上是对文件的预览方式,可以分为一次渲染和二次渲染;一般而言二次渲染后的图片很难攻入,很难在图片中嵌入代码,因此个人建议使用二次渲染,至少应该一次渲染,如果渲染失败,可以认为该文件是非法文件。不让其上传。
在本文中,不介绍文件加载检测过程,将在后续文章中介绍图片渲染的方法。
转载于:https://blog.51cto.com/793404905/1572268