1. 程式人生 > 資料庫 >request請求的body中的引數(json物件)只能取出一次,引數丟失問題的解決方式(防sql注入過濾器的應用)

request請求的body中的引數(json物件)只能取出一次,引數丟失問題的解決方式(防sql注入過濾器的應用)

在專案即將上線的滲透測試報告中檢測出了sql注入的問題,關於這個問題的解決方案,最初的思路是寫一個全域性的過濾器,對所有請求的引數進行過濾攔截,如果存在和sql注入相關的特殊字元則攔截掉,具體細節展開以下討論!

(當然要提供一個白名單,白名單裡的請求不給予過濾)

首先提供以下白名單code.properties
# 鑑權碼
# IDAM鑑權(多個以逗號分隔)
authcode=32j42i3
# 防sql注入請求白名單
sqlverify=/ryjh/mappingGroup/updateInfo,\
  /author/Logon/loginConfigCheck,\
  /author/Logon/login,\
  /author/SAuUser/resetPwd,\
  /author/SAuUser/addUser,\
  /swagger-resources/configuration/ui,\
  /swagger-resources,\
  /doc.html
第一版的過濾器如下
/**
 * @author FanJiangFeng
 * @version 1.0.0
 * @ClassName SqlFilter.java
 * @Description 防止Sql注入過濾器,校驗引數
 * @createTime 2021年01月05日 17:08:00
 */
@Component
@WebFilter(value = "/")
public class SqlFilter implements Filter {

    //Sql注入配置檔案白名單絕對路徑
    @Value("${auth.authCodeUrl}")
    private String url;


    private boolean verify(String uri) throws IOException {
        Properties properties=new Properties();
        InputStream inputStream=new FileInputStream(new File(url));
        properties.load(inputStream);
        Map<String,String> codeMap=(Map)properties;
        String whiteDoc=codeMap.get("sqlverify");
        String[] strings = whiteDoc.split(",");
        boolean over=false;
        for(String s:strings){
            if(s.equals(uri)){
                over=true;
                break;
            }
        }
        return over;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request=(HttpServletRequest)servletRequest;
        String contentType = request.getContentType();
        String requestURI = request.getRequestURI();
        boolean verify = verify(requestURI);
        if(verify){
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }   
            //application/x-www-form-urlencoded
            Map<String, String[]> parameterMap = request.getParameterMap();
            for(Map.Entry<String,String[]> entry:parameterMap.entrySet()){
//                String strings = entry.getKey();
                //校驗引數名是否合法
//            boolean isTrue = verifySql(strings);
//            if(!isTrue){
//                return;
//            }
                //校驗引數值是否合法
                String[] value = entry.getValue();
                for(String s:value){
                    //校驗引數值是否合法
                    boolean b = verifySql(s);
                    if(!b){
                        return;
                    }
                }
        }
        
            filterChain.doFilter(servletRequest,servletResponse);
        	return;
    }

    @Override
    public void destroy() {

    }

    /**
     * 校驗引數非法字元
     */
    public boolean verifySql(String parameter){
        if(parameter.contains("'")){ //' 單引號
            return false;
        }else if(parameter.contains("\"")){ //" 雙引號
            return false;
        }else if(parameter.contains("\\'")){//' 反斜槓單引號
            return false;
        }else if(parameter.contains("\\\"")){//" 反斜槓雙引號
            return false;
        }else if(parameter.contains("(")||parameter.contains(")")||parameter.contains(";")){//括號和分號
            return false;
        }else if(parameter.contains("--")||parameter.contains("+")){//雙減號 加號
            return false;
        }else if(parameter.toLowerCase().contains("select")||parameter.toLowerCase().contains("update")
        ||parameter.toLowerCase().contains("delete")||parameter.toLowerCase().contains("drop")
        ||parameter.toLowerCase().contains("updatexml")||parameter.toLowerCase().contains("concat")){
            return false;
        }
        return true;

    }
}

第一個版本的不足:

它只能解析content-type為application/x-www-form-urlencoded的請求攜帶的引數

由Map<String, String[]> parameterMap = request.getParameterMap()的方式進行獲取

但是它解析不了content-type型別為application/json格式的引數 ,上面那種方式已經獲取不到了,所以要重新改版。

我是如何跳坑的?

剛開始我新加了一個方法,傳入request物件,然後從request物件中拿到json字串格式的引數,通過對字串進行轉換校驗等處理,然後達到目的效果,但是我發現,處理之後,雖然過濾器放開了這個請求,當請求來到controller時,引數消失了?

這是因為,request請求中的body引數只可以拿出來一次,拿出來就沒有了!

解決方案

需要一個類繼承HttpServletRequestWrapper,該類繼承了ServletRequestWrapper並實現了HttpServletRequest,

因此它可作為request在FilterChain中傳遞。

該類需要重寫getReader和getInputStream兩個方法,並在返回時將讀出的body資料重新寫入。

參考文章:

新建BodyReaderRequestWrapper類
public class BodyReaderRequestWrapper extends HttpServletRequestWrapper {
    private final String body;

    public String getBody() {
        return body;
    }

    /**
     * 取出請求體body中的引數(建立物件時執行)
     * @param request
     */
    public BodyReaderRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        StringBuilder sb = new StringBuilder();
        InputStream ins = request.getInputStream();
        BufferedReader isr = null;
        try{
            if(ins != null){
                isr = new BufferedReader(new InputStreamReader(ins));
                char[] charBuffer = new char[128];
                int readCount = 0;
                while((readCount = isr.read(charBuffer)) != -1){
                    sb.append(charBuffer,0,readCount);
                }
            }else{
                sb.append("");
            }
        }catch (IOException e){
            throw e;
        }finally {
            if(isr != null) {
                isr.close();
            }
        }

        sb.toString();
        body = sb.toString();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayIns = new ByteArrayInputStream(body.getBytes());
        ServletInputStream servletIns = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener readListener) {

            }
            @Override
            public int read() throws IOException {
                return byteArrayIns.read();
            }
        };
        return  servletIns;
    }
}
filter過濾器更改

在dofilter方法中建立BodyReaderRequestWrapper物件,並繼續傳遞。

		BodyReaderRequestWrapper wrapper=null;
        if("application/json".equals(contentType)){
            wrapper=new BodyReaderRequestWrapper(request);
            ......
                
        if(wrapper==null){
            filterChain.doFilter(servletRequest,servletResponse);
        }else{
            filterChain.doFilter(wrapper,servletResponse);
        }

既然可以獲取到json物件的字串資訊了,那麼開始寫對json的校驗過程

對json格式引數遞迴解析

討論:json格式的引數種類很多,比如

{
"id":"test",
"name":"test"
}
[
    {
        "id":"test",
        "name":"test"
	}
    {
        "id":"test",
        "name":"test"
	}
]
{
"id":"test",
"name":[
            {
                "id":"test",
                "name":"test"
            }
            {
                "id":"test",
                "name":"test"
            }
		]
}

以及更多,所以這裡採用遞迴解析的方式

過濾器的最終版本
@Component
@WebFilter(value = "/")
public class SqlFilter implements Filter {

    //Sql注入配置檔案白名單絕對路徑
    @Value("${auth.authCodeUrl}")
    private String url;


    private boolean verify(String uri) throws IOException {
        Properties properties=new Properties();
        InputStream inputStream=new FileInputStream(new File(url));
        properties.load(inputStream);
        Map<String,String> codeMap=(Map)properties;
        String whiteDoc=codeMap.get("sqlverify");
        String[] strings = whiteDoc.split(",");
        boolean over=false;
        for(String s:strings){
            if(s.equals(uri)){
                over=true;
                break;
            }
        }
        return over;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request=(HttpServletRequest)servletRequest;
        String contentType = request.getContentType();
        String requestURI = request.getRequestURI();
        boolean verify = verify(requestURI);
        if(verify){
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }
        BodyReaderRequestWrapper wrapper=null;
        if("application/json".equals(contentType)){
            wrapper=new BodyReaderRequestWrapper(request);
            String requestPostStr = wrapper.getBody();
            if (requestPostStr.startsWith("{")) {
                //解析json物件
                boolean b = resolveJSONObjectObj(requestPostStr);
                if(!b)return;

            }else if (requestPostStr.startsWith("[")) {
                //把資料轉換成json陣列
                JSONArray jsonArray = JSONArray.parseArray(requestPostStr);
                jsonArray.forEach(json -> {
                    //解析json物件
                    boolean b = resolveJSONObjectObj(json.toString());
                    if(!b)return;
                });
            }

        }else{
            //application/x-www-form-urlencoded
            Map<String, String[]> parameterMap = request.getParameterMap();
            for(Map.Entry<String,String[]> entry:parameterMap.entrySet()){
//                String strings = entry.getKey();
                //校驗引數名是否合法
//            boolean isTrue = verifySql(strings);
//            if(!isTrue){
//                return;
//            }
                //校驗引數值是否合法
                String[] value = entry.getValue();
                for(String s:value){
                    //校驗引數值是否合法
                    boolean b = verifySql(s);
                    if(!b){
                        return;
                    }
                }
            }
        }
        if(wrapper==null){
            filterChain.doFilter(servletRequest,servletResponse);
        }else{
            filterChain.doFilter(wrapper,servletResponse);

        }
        return;
    }

    /**
     * 對JSONObject物件進行遞迴引數解析
     *
     * @param requestPostStr
     * @return
     */
    private boolean resolveJSONObjectObj(String requestPostStr) {
        boolean isover=true;
        // 建立需要處理的json物件
        JSONObject jsonObject = JSONObject.parseObject(requestPostStr);
        // 獲取所有的引數key
        Set<String> keys = jsonObject.keySet();
        if (keys.size() > 0) {
            for (String key : keys) {
                //獲取引數名稱
                String value = null;
                if (jsonObject.get(key) != null) {
                    value = String.valueOf(jsonObject.get(key));
                    //當value為陣列時
                    if(value.startsWith("[")){
                        //把資料轉換成json陣列
                        JSONArray jsonArray = JSONArray.parseArray(value);
                        for(int i=0;i<jsonArray.size();i++){
                            //解析json物件
                            boolean b = resolveJSONObjectObj(jsonArray.get(i).toString());
                            if(!b){
                                isover=false;
                                break;
                            }
                        }
                    }else if(value.startsWith("{")){
                        boolean b = resolveJSONObjectObj(value);
                        if(!b){
                            isover=false;
                            break;
                        }
                    }else{
                        //校驗引數值是否合法
                        boolean b = verifySql(value);
                        if(!b){
                            isover=false;
                            break;
                        }
                    }
                }
            }
        }
        return isover;
    }


    @Override
    public void destroy() {

    }

    /**
     * 校驗引數非法字元
     */
    public boolean verifySql(String parameter){
        if(parameter.contains("'")){ //' 單引號
            return false;
        }else if(parameter.contains("\"")){ //" 雙引號
            return false;
        }else if(parameter.contains("\\'")){//' 反斜槓單引號
            return false;
        }else if(parameter.contains("\\\"")){//" 反斜槓雙引號
            return false;
        }else if(parameter.contains("(")||parameter.contains(")")||parameter.contains(";")){//括號和分號
            return false;
        }else if(parameter.contains("--")||parameter.contains("+")){//雙減號 加號
            return false;
        }else if(parameter.toLowerCase().contains("select")||parameter.toLowerCase().contains("update")
        ||parameter.toLowerCase().contains("delete")||parameter.toLowerCase().contains("drop")
        ||parameter.toLowerCase().contains("updatexml")||parameter.toLowerCase().contains("concat")){
            return false;
        }
        return true;

    }
}

這樣,什麼格式的json引數都會解析到!如果有任何問題可以聯絡本人,可以共同探討!