44. 從零開(kāi)始學(xué)springboot擼一個(gè)Xss過(guò)濾器-Filter實(shí)現(xiàn)

前言

項(xiàng)目安全需要, 需要全局對(duì)參數(shù)進(jìn)行xss過(guò)濾處理.

Xss簡(jiǎn)介

關(guān)于Xss很多人可能都有了解, 出于“禮貌”,

咸魚(yú)君還是簡(jiǎn)單舉個(gè)例子

用戶注冊(cè)時(shí)可以填寫(xiě)姓名

此時(shí)我填寫(xiě)了“<script>alert(1);</script>” 并提交,

后端呢沒(méi)有做任何檢測(cè)就保存了.

那么就可能有問(wèn)題, 下次再訪問(wèn)這個(gè)頁(yè)面你會(huì)發(fā)現(xiàn)不停的彈窗“1”!

Xss攻擊呢也分很多種, 感興趣的自行查閱資料, 這里不多說(shuō)了.

那么如何面對(duì)Xss攻擊呢?

其實(shí)也很好解決, 一句話“不要相信用戶的任何輸入”!

編程上就是對(duì)用戶的提交內(nèi)容進(jìn)行過(guò)濾以及非法字符的“轉(zhuǎn)義”!

Springboot Xss攔截器實(shí)現(xiàn)

對(duì)整個(gè)系統(tǒng)的提交進(jìn)行過(guò)濾和轉(zhuǎn)義, 如過(guò)針對(duì)每個(gè)點(diǎn)分別調(diào)用某個(gè)方法去做這件事, 會(huì)顯得很麻煩, 而且, 重復(fù)的代碼看著也不美觀, 所以, 這里就采用Springboot+Filter的方式實(shí)現(xiàn)一個(gè)Xss的全局過(guò)濾器.

Springboot實(shí)現(xiàn)一個(gè)Xss過(guò)濾器, 常用的有兩種方式:

  • 重寫(xiě)HttpServletRequestWrapper
    重寫(xiě)getHeader()、getParameter()、getParameterValues()、getInputStream()實(shí)現(xiàn)對(duì)傳統(tǒng)“鍵值對(duì)”傳參方式的過(guò)濾
    重寫(xiě)getInputStream()實(shí)現(xiàn)對(duì)Json方式傳參的過(guò)濾,也就是@RequestBody參數(shù).
  • 自定義序列化器, 對(duì)MappingJackson2HttpMessageConverter 的objectMapper做設(shè)置.
    重寫(xiě)JsonSerializer.serialize()實(shí)現(xiàn)對(duì)出參的過(guò)濾 (PS: 數(shù)據(jù)原樣保存, 取出來(lái)的時(shí)候轉(zhuǎn)義)
    重寫(xiě)JsonDeserializer.deserialize()實(shí)現(xiàn)對(duì)入?yún)⒌倪^(guò)濾 (PS: 數(shù)據(jù)轉(zhuǎn)義后保存)

針對(duì)以上兩種方式有幾個(gè)注意點(diǎn):

問(wèn)題一: json參數(shù)(@RequestBody)的處理

針對(duì)Json方式傳參 ,
springmvc默認(rèn)使用jackjson做序列化.
重寫(xiě)getInputStream()來(lái)實(shí)現(xiàn)xss過(guò)濾時(shí),
如果你對(duì)參數(shù)替換了雙引號(hào), jackjson序列/反序列化參數(shù)時(shí)會(huì)報(bào)錯(cuò),
因?yàn)樗徽J(rèn)識(shí)這個(gè)格式!
所以我們對(duì)參數(shù)處理時(shí)要剔除雙引號(hào)!
或者, 你選擇自定義序列化器來(lái)實(shí)現(xiàn)json參數(shù)的處理.

問(wèn)題二: 自定義序列化器

雖然我們?nèi)雲(yún)⒑统鰠煞N方式都寫(xiě)了案例,
實(shí)際上只需要選擇其一來(lái)做即可!沒(méi)必要兩種都做過(guò)濾.
這里推薦對(duì)入?yún)⑦M(jìn)行處理, 理由如下:

  1. 重寫(xiě)getHeader()、getParameter()、getParameterValues()是直接對(duì)入?yún)⑦M(jìn)行轉(zhuǎn)義,
    也就是你后續(xù)程序獲取的參數(shù)都是轉(zhuǎn)義后的,
    所以, 為了全局統(tǒng)一, 我們對(duì)@RequestBody類型的參數(shù)也進(jìn)行入?yún)⑻幚?

  2. 對(duì)入?yún)⑥D(zhuǎn)義意味著保存進(jìn)DB的就是“安全”的數(shù)據(jù)

本次案例實(shí)現(xiàn)原理如下:

  • 針對(duì)“鍵值對(duì)”參數(shù)采用重寫(xiě)HttpServletRequestWrapper過(guò)濾
  • 針對(duì)json參數(shù)采用自定義序列化器來(lái)過(guò)濾

PS: 針對(duì)json參數(shù)也實(shí)現(xiàn)了getInputStream()方式的過(guò)濾(代碼為注釋狀態(tài))

實(shí)現(xiàn)

首先, 我們先引入一個(gè)工具包, 就是大名鼎鼎的“糊涂”工具包, 該包集成了大量的好用的工具!強(qiáng)烈推薦使用!

<!--HuTool工具包 -->
<dependency>
     <groupId>cn.hutool</groupId>
     <artifactId>hutool-all</artifactId>
     <version>5.2.3</version>
</dependency>

我們用到HuTool內(nèi)的EscapeUtil這個(gè)工具來(lái)轉(zhuǎn)義特殊字符.

使用HttpServletRequestWrapper重寫(xiě)Request請(qǐng)求參數(shù)

package com.mrcoder.sbxssfilter.config.xss;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.EscapeUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * XSS過(guò)濾處理
 */
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    /**
     * 描述 : 構(gòu)造函數(shù)
     *
     * @param request 請(qǐng)求對(duì)象
     */
    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }


    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        return EscapeUtil.escape(value);
    }

    //重寫(xiě)getParameter
    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        return EscapeUtil.escape(value);
    }

    //重寫(xiě)getParameterValues
    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values != null) {
            int length = values.length;
            String[] escapseValues = new String[length];
            for (int i = 0; i < length; i++) {
                escapseValues[i] = EscapeUtil.escape(values[i]);
            }
            return escapseValues;
        }
        return super.getParameterValues(name);
    }

//    //重寫(xiě)getInputStream,對(duì)json格式參數(shù)進(jìn)行過(guò)濾(也就是@RequestBody類型的參數(shù))
//    @Override
//    public ServletInputStream getInputStream() throws IOException {
//        // 非json類型,直接返回
//        if (!super.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
//            return super.getInputStream();
//        }
//
//        // 為空,直接返回
//        String json = IoUtil.read(super.getInputStream(), "utf-8");
//        if (StrUtil.isEmpty(json)) {
//            return super.getInputStream();
//        }
//        // 這里要注意,json格式的參數(shù)不能直接使用hutool的EscapeUtil.escape, 因?yàn)樗鼤?huì)把"也給轉(zhuǎn)義,
//        // 使得@RequestBody沒(méi)辦法解析成為一個(gè)正常的對(duì)象,所以我們自己實(shí)現(xiàn)一個(gè)過(guò)濾方法
//        // 或者采用定制自己的objectMapper處理json出入?yún)⒌霓D(zhuǎn)義(推薦使用)
//        json = cleanXSS(json).trim();
//        final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
//        return new ServletInputStream() {
//            @Override
//            public boolean isFinished() {
//                return true;
//            }
//
//            @Override
//            public boolean isReady() {
//                return true;
//            }
//
//            @Override
//            public void setReadListener(ReadListener readListener) {
//            }
//
//            @Override
//            public int read() {
//                return bis.read();
//            }
//        };
//    }
//
//    public static String cleanXSS(String value) {
//        value = value.replaceAll("&", "%26");
//        value = value.replaceAll("<", "%3c");
//        value = value.replaceAll(">", "%3e");
//        value = value.replaceAll("'", "%27");
//        //value = value.replaceAll(":", "%3a");
//        //value = value.replaceAll("\"", "%22");
//        //value = value.replaceAll("/", "%2f");
//        return value;
//    }

}

實(shí)現(xiàn)一個(gè)過(guò)濾器

package com.mrcoder.sbxssfilter.config.xss;

import cn.hutool.core.util.StrUtil;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 防止XSS攻擊的過(guò)濾器
 */
@Component
public class XssFilter implements Filter {
    /**
     * 排除鏈接
     */
    private List<String> excludes = new ArrayList<>();

    /**
     * xss過(guò)濾開(kāi)關(guān)
     */
    private boolean enabled = false;

    @Override
    public void init(FilterConfig filterConfig) {
        String tempExcludes = filterConfig.getInitParameter("excludes");
        String tempEnabled = filterConfig.getInitParameter("enabled");
        if (StrUtil.isNotEmpty(tempExcludes)) {
            String[] url = tempExcludes.split(",");
            Collections.addAll(excludes, url);
        }
        if (StrUtil.isNotEmpty(tempEnabled)) {
            enabled = Boolean.valueOf(tempEnabled);
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        if (handleExcludeUrl(req)) {
            chain.doFilter(request, response);
            return;
        }
        XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
        chain.doFilter(xssRequest, response);
    }

    /**
     * 判斷當(dāng)前路徑是否需要過(guò)濾
     */
    private boolean handleExcludeUrl(HttpServletRequest request) {
        if (!enabled) {
            return true;
        }
        if (excludes == null || excludes.isEmpty()) {
            return false;
        }
        String url = request.getServletPath();
        for (String pattern : excludes) {
            Pattern p = Pattern.compile("^" + pattern);
            Matcher m = p.matcher(url);
            if (m.find()) {
                return true;
            }
        }
        return false;
    }
}

配置并注冊(cè)過(guò)濾器

package com.mrcoder.sbxssfilter.config.xss;

import javax.servlet.DispatcherType;

import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author xssfilter配置
 */
@Configuration
public class XssFilterConfig {
    @Value("${xss.enabled}")
    private String enabled;

    @Value("${xss.excludes}")
    private String excludes;

    @Value("${xss.urlPatterns}")
    private String urlPatterns;

    @SuppressWarnings({"rawtypes", "unchecked"})
    @Bean
    public FilterRegistrationBean xssFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setDispatcherTypes(DispatcherType.REQUEST);
        registration.setFilter(new XssFilter());
        //添加過(guò)濾路徑
        registration.addUrlPatterns(StrUtil.split(urlPatterns, ","));
        registration.setName("xssFilter");
        registration.setOrder(Integer.MAX_VALUE);
        //設(shè)置初始化參數(shù)
        Map<String, String> initParameters = new HashMap<String, String>();
        initParameters.put("excludes", excludes);
        initParameters.put("enabled", enabled);
        registration.setInitParameters(initParameters);
        return registration;
    }

    /**
     * 過(guò)濾json類型的
     *
     * @param builder
     * @return
     */
    @Bean
    @Primary
    public ObjectMapper xssObjectMapper(Jackson2ObjectMapperBuilder builder) {
        //解析器
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        //注冊(cè)xss解析器
        SimpleModule xssModule = new SimpleModule("XssStringJsonDeserializer");

        //入?yún)⒑统鰠⑦^(guò)濾選一個(gè)就好了,沒(méi)必要兩個(gè)都加
        //這里為了和XssHttpServletRequestWrapper統(tǒng)一,建議對(duì)入?yún)⑦M(jìn)行處理
        //注冊(cè)入?yún)⑥D(zhuǎn)義
        xssModule.addDeserializer(String.class, new XssStringJsonDeserializer());
        //注冊(cè)出參轉(zhuǎn)義
        //xssModule.addSerializer(new XssStringJsonSerializer());
        objectMapper.registerModule(xssModule);
        //返回
        return objectMapper;
    }
}

最后,我們?cè)谂渲梦募由?/p>

#是否打開(kāi)
xss.enabled=true
#不過(guò)濾路徑, 以逗號(hào)分割
xss.excludes=/open/*,/open2/*
//過(guò)濾路徑, 逗號(hào)分割
xss.urlPatterns=/*

另外, 我們需要實(shí)現(xiàn)一個(gè)ObjectMapper來(lái)處理json格式的參數(shù)

處理json入?yún)⒌霓D(zhuǎn)義

package com.mrcoder.sbxssfilter.config.xss;

import cn.hutool.core.util.EscapeUtil;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

import java.io.IOException;

/**
 * 處理json入?yún)⒌霓D(zhuǎn)義
 */
public class XssStringJsonDeserializer extends JsonDeserializer<String> {

    @Override
    public Class<String> handledType() {
        return String.class;
    }

    //對(duì)入?yún)⑥D(zhuǎn)義
    @Override
    public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        String value = jsonParser.getText();
        if (value != null) {
            return EscapeUtil.escape(value);
        }
        return value;
    }

}

處理json出參的轉(zhuǎn)義

package com.mrcoder.sbxssfilter.config.xss;

import cn.hutool.core.util.EscapeUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;

/**
 * 處理json出參的轉(zhuǎn)義
 */
public class XssStringJsonSerializer extends JsonSerializer<String> {

    @Override
    public Class<String> handledType() {
        return String.class;
    }

    //對(duì)出參轉(zhuǎn)義
    @Override
    public void serialize(String value, JsonGenerator jsonGenerator,
                          SerializerProvider serializerProvider) throws IOException {
        if (value != null) {
            String encodedValue = EscapeUtil.escape(value);
            jsonGenerator.writeString(encodedValue);
        }
    }

}

寫(xiě)個(gè)測(cè)試

package com.mrcoder.sbxssfilter.model;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class People {
    private String name;
    private String info;
}

package com.mrcoder.sbxssfilter.controller;


import com.mrcoder.sbxssfilter.model.People;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class XssController {

    //鍵值對(duì)
    @PostMapping("xssFilter")
    public String xssFilter(String name, String info) {
        log.error(name + "---" + info);
        return name + "---" + info;
    }
    //實(shí)體
    @PostMapping("modelXssFilter")
    public People modelXssFilter(@RequestBody People people) {
        log.error(people.getName() + "---" + people.getInfo());
        return people;
    }

    //不轉(zhuǎn)義
    @PostMapping("open/xssFilter")
    public String openXssFilter(String name) {
        return name;
    }

    //不轉(zhuǎn)義2
    @PostMapping("open2/xssFilter")
    public String open2XssFilter(String name) {
        return name;
    }
}

json方式


image.png

鍵值對(duì)方式


image.png

項(xiàng)目地址

https://github.com/MrCoderStack/SpringBootDemo/tree/master/sb-xssfilter

請(qǐng)關(guān)注我的訂閱號(hào)

訂閱號(hào).png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容