学习笔记-java代码审计-文件操作

java代码审计-文件操作

0x01 文件上传

这段代码来自菜鸟教程:

private static final String UPLOAD_PATH = "/tmp/upload";
private boolean uploadWithFileUpload(HttpServletRequest request) {
  if(!ServletFileUpload.isMultipartContent(request)) {
    //response.getWriter().print("Error: 表单必须包含 enctype=multipart/form-data");
    return false;
  }
	/*省略部分初始化代码*/
  try {
    List<FileItem> fileItems = servletFileUpload.parseRequest(request);
    if(fileItems != null && fileItems.size() > 0) {
      for(FileItem fileItem : fileItems) {
        String fileName = new File(fileItem.getName()).getName(); //获取文件名
        String filePath = UPLOAD_PATH + File.separator + fileName; //上传路径
        File storeFile = new File(filePath);
        fileItem.write(storeFile); //写文件
      }
      //response.getWriter().println("upload success");
    }
  } catch (Exception e) {
    //response.getWriter().print(e.getMessage());
    return false;
  }
  return true;
}

标准的任意文件上传代码。。。现在的web框架会将上传的文件放在web访问不到的位置存储,这样会一定程度上缓解上传漏洞,但如果在展示文件时处理不当,就会导致任意文件读取或下载。

servlet3之后,可以不使用commons-fileupload、commons-io这两个jar包来处理文件上传,转而使用request.getParts()获取上传文件。此外,servlet3还支持以注解的形式定义一些上传的属性。

private boolean uploadWithAnnotation(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
    Part part = request.getPart("fileName");
    if(part == null) {
      return false;
    }
    String filename = UPLOAD_PATH + File.separator + part.getSubmittedFileName();
    part.write(filename);
    part.delete();
    return true;
}

可以看到这种方法代码比较简洁,需要注意的是要在类前面加@MultipartConfig这个注解,否则会报错。

防御

文件上传参考这个https://mp.weixin.qq.com/s/LzVseWzLP3CjQsmYzvELZA

  1. 判断文件content-type

  2. 文件上传后改名

    String fileExt = filename.substring(filename.lastIndexOf(".") + 1); //获取后缀名
    String savename = UPLOAD_PATH + File.separator + DigestUtils.md5Hex(System.currentTimeMillis() + filename) + "." + fileExt;
    
  3. 设置严格的后缀名白名单

    使用esapi的后缀名检测,需要在对应的resource目录建立ESAPI.propertiesvalidation.properties,文件内容可以留空,也可以下载官方内容https://github.com/ESAPI/esapi-java-legacy/blob/develop/src/test/resources/esapi

    //检测后缀名
    if(!ESAPI.validator().isValidFileName("upload", filename, ALLOW_EXT, false)) {
        response.getWriter().println("后缀名不合法");
    }
    
  4. 对文件大小进行限制

    基于ServletFileUpload

    DiskFileItemFactory factory = new DiskFileItemFactory();
    ServletFileUpload servletFileUpload = new ServletFileUpload(factory);
    servletFileUpload.setSizeMax(1024 * 400);
    

    基于注解( version > servlet3)

    @MultipartConfig(maxFileSize = 1000 * 1024 * 1024, maxRequestSize = 1000 * 1024 * 1024)
    public class UploadServlet extends HttpServlet {}
    
  5. 条件允许的情况下,将文件放到web访问不到的位置存储

0x02 文件操作(下载/读取/删除)

这几个操作比较类似,都是对文件的操作。

java的文件读取一般有两种方法,一种是基于InputStream,另一种是基于FileReader。

读取

//InputStream
File file = new File(filename);
InputStream inputStream = new FileInputStream(file);
while(-1 != (len = inputStream.read())) {
  	outputStream.write(len);
}
//FileReader
String fileContent = "";
FileReader fileReader = new FileReader(filename);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line = "";
while (null != (line = bufferedReader.readLine())) {
		fileContent += (line + "\n");
}

下载

读写操作都有的情况下,用流比较方便。

//stream
String filename = request.getParameter("filename");
File file = new File(filename);

response.reset();
response.addHeader("Content-Disposition", "attachment;filename=" + new String(filename.getBytes("utf-8")));
response.addHeader("Content-Length", "" + file.length());
response.setContentType("application/octet-stream; charset=utf-8");

InputStream inputStream = new FileInputStream(file);
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
int len;
while(-1 != (len = inputStream.read())) {
  outputStream.write(len);
}

inputStream.close();
outputStream.close();

删除

String filename = request.getParameter("filename");
File file = new File(filename);
if(file != null && file.exists() && file.delete()) {
  	response.getWriter().println("delete success");
} else {
  	response.getWriter().println("delete failed");
}

防御

防御的核心在于对传入的文件名进行正确的过滤,防止跨目录。

private boolean checkFilename(String filename) {
    filename = filename.replace("\\", "/"); //消除平台差异
  	//将文件操作限定在指定目录中
  	File file = new File(filename);
    if(file == null || !file.exists() || file.getCanonicalFile().getParent().equals(new File(DOWNLOAD_PATH).getCanonicalPath())) { //检测上级目录是否为指定目录
      return false;
    }
  	//检测文件名中是否有敏感字符
    List<String> blackList = Arrays.asList("./", "../");
    for(String badChar : blackList) {
      if(filename.contains(badChar)) {
        return false;
      }
    }
  	//对文件后缀名设置白名单
    List<String> whiteExt = Arrays.asList("png", "jpg", "gif", "jpeg", "doc");
    String fileExt = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
    if(!whiteExt.contains(fileExt)) {
      return false;
    }
    return true;
}

0x03 文件写入

//FileWriter
FileWriter fileWriter = new FileWriter(filename);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write(fileContent);
bufferedWriter.close();

防御

文件写入与上述三个操作略有不同,在防御时需要对写入的内容进行过滤。

private boolean checkFilecontent(String fileContent) {
    List<String> blackContents = Arrays.asList("<%@page", "<%!");
    for(String blackContent : blackContents) {
      if(fileContent.contains(blackContent)) {
        return false;
      }
    }
    return true;
}

在写入时最好将标签进行htmlencode

String fileContent = request.getParameter("content");
fileContent = ESAPI.encoder().encodeForHTML(fileContent);

点击关注,共同学习!
安全狗的自我修养

github haidragon

https://github.com/haidragon

posted @ 2022-11-13 08:34  syscallwww  阅读(96)  评论(0编辑  收藏  举报