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
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
-
判断文件content-type
-
文件上传后改名
- String fileExt = filename.substring(filename.lastIndexOf(".") + 1); //获取后缀名
- String savename = UPLOAD_PATH + File.separator + DigestUtils.md5Hex(System.currentTimeMillis() + filename) + "." + fileExt;
-
设置严格的后缀名白名单
使用esapi的后缀名检测,需要在对应的resource目录建立
ESAPI.properties和validation.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("后缀名不合法");
- }
-
对文件大小进行限制
基于
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 {}
-
条件允许的情况下,将文件放到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
blackList = Arrays.asList("./", "../"); - for(String badChar : blackList) {
- if(filename.contains(badChar)) {
- return false;
- }
- }
- //对文件后缀名设置白名单
- List
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
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);

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