44. 从零开始学springboot撸一个Xss过滤器-Filter实现

前言

项目安全需要, 需要全局对参数进行xss过滤处理.

Xss简介

关于Xss很多人可能都有了解, 出于“礼貌”,

咸鱼君还是简单举个例子

用户注册时可以填写姓名

此时我填写了“<script>alert(1);</script>” 并提交,

后端呢没有做任何检测就保存了.

那么就可能有问题, 下次再访问这个页面你会发现不停的弹窗“1”!

Xss攻击呢也分很多种, 感兴趣的自行查阅资料, 这里不多说了.

那么如何面对Xss攻击呢?

其实也很好解决, 一句话“不要相信用户的任何输入”!

编程上就是对用户的提交内容进行过滤以及非法字符的“转义”!

Springboot Xss拦截器实现

对整个系统的提交进行过滤和转义, 如过针对每个点分别调用某个方法去做这件事, 会显得很麻烦, 而且, 重复的代码看着也不美观, 所以, 这里就采用Springboot+Filter的方式实现一个Xss的全局过滤器.

Springboot实现一个Xss过滤器, 常用的有两种方式:

  • 重写HttpServletRequestWrapper
    重写getHeader()、getParameter()、getParameterValues()、getInputStream()实现对传统“键值对”传参方式的过滤
    重写getInputStream()实现对Json方式传参的过滤,也就是@RequestBody参数.
  • 自定义序列化器, 对MappingJackson2HttpMessageConverter 的objectMapper做设置.
    重写JsonSerializer.serialize()实现对出参的过滤 (PS: 数据原样保存, 取出来的时候转义)
    重写JsonDeserializer.deserialize()实现对入参的过滤 (PS: 数据转义后保存)

针对以上两种方式有几个注意点:

问题一: json参数(@RequestBody)的处理

针对Json方式传参 ,
springmvc默认使用jackjson做序列化.
重写getInputStream()来实现xss过滤时,
如果你对参数替换了双引号, jackjson序列/反序列化参数时会报错,
因为它不认识这个格式!
所以我们对参数处理时要剔除双引号!
或者, 你选择自定义序列化器来实现json参数的处理.

问题二: 自定义序列化器

虽然我们入参和出参两种方式都写了案例,
实际上只需要选择其一来做即可!没必要两种都做过滤.
这里推荐对入参进行处理, 理由如下:

  1. 重写getHeader()、getParameter()、getParameterValues()是直接对入参进行转义,
    也就是你后续程序获取的参数都是转义后的,
    所以, 为了全局统一, 我们对@RequestBody类型的参数也进行入参处理.

  2. 对入参转义意味着保存进DB的就是“安全”的数据

本次案例实现原理如下:

  • 针对“键值对”参数采用重写HttpServletRequestWrapper过滤
  • 针对json参数采用自定义序列化器来过滤

PS: 针对json参数也实现了getInputStream()方式的过滤(代码为注释状态)

实现

首先, 我们先引入一个工具包, 就是大名鼎鼎的“糊涂”工具包, 该包集成了大量的好用的工具!强烈推荐使用!

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

我们用到HuTool内的EscapeUtil这个工具来转义特殊字符.

使用HttpServletRequestWrapper重写Request请求参数

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过滤处理
 */
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    /**
     * 描述 : 构造函数
     *
     * @param request 请求对象
     */
    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }


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

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

    //重写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);
    }

//    //重写getInputStream,对json格式参数进行过滤(也就是@RequestBody类型的参数)
//    @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格式的参数不能直接使用hutool的EscapeUtil.escape, 因为它会把"也给转义,
//        // 使得@RequestBody没办法解析成为一个正常的对象,所以我们自己实现一个过滤方法
//        // 或者采用定制自己的objectMapper处理json出入参的转义(推荐使用)
//        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;
//    }

}

实现一个过滤器

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攻击的过滤器
 */
@Component
public class XssFilter implements Filter {
    /**
     * 排除链接
     */
    private List<String> excludes = new ArrayList<>();

    /**
     * xss过滤开关
     */
    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);
    }

    /**
     * 判断当前路径是否需要过滤
     */
    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;
    }
}

配置并注册过滤器

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());
        //添加过滤路径
        registration.addUrlPatterns(StrUtil.split(urlPatterns, ","));
        registration.setName("xssFilter");
        registration.setOrder(Integer.MAX_VALUE);
        //设置初始化参数
        Map<String, String> initParameters = new HashMap<String, String>();
        initParameters.put("excludes", excludes);
        initParameters.put("enabled", enabled);
        registration.setInitParameters(initParameters);
        return registration;
    }

    /**
     * 过滤json类型的
     *
     * @param builder
     * @return
     */
    @Bean
    @Primary
    public ObjectMapper xssObjectMapper(Jackson2ObjectMapperBuilder builder) {
        //解析器
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        //注册xss解析器
        SimpleModule xssModule = new SimpleModule("XssStringJsonDeserializer");

        //入参和出参过滤选一个就好了,没必要两个都加
        //这里为了和XssHttpServletRequestWrapper统一,建议对入参进行处理
        //注册入参转义
        xssModule.addDeserializer(String.class, new XssStringJsonDeserializer());
        //注册出参转义
        //xssModule.addSerializer(new XssStringJsonSerializer());
        objectMapper.registerModule(xssModule);
        //返回
        return objectMapper;
    }
}

最后,我们在配置文件加上

#是否打开
xss.enabled=true
#不过滤路径, 以逗号分割
xss.excludes=/open/*,/open2/*
//过滤路径, 逗号分割
xss.urlPatterns=/*

另外, 我们需要实现一个ObjectMapper来处理json格式的参数

处理json入参的转义

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入参的转义
 */
public class XssStringJsonDeserializer extends JsonDeserializer<String> {

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

    //对入参转义
    @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出参的转义

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出参的转义
 */
public class XssStringJsonSerializer extends JsonSerializer<String> {

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

    //对出参转义
    @Override
    public void serialize(String value, JsonGenerator jsonGenerator,
                          SerializerProvider serializerProvider) throws IOException {
        if (value != null) {
            String encodedValue = EscapeUtil.escape(value);
            jsonGenerator.writeString(encodedValue);
        }
    }

}

写个测试

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 {

    //键值对
    @PostMapping("xssFilter")
    public String xssFilter(String name, String info) {
        log.error(name + "---" + info);
        return name + "---" + info;
    }
    //实体
    @PostMapping("modelXssFilter")
    public People modelXssFilter(@RequestBody People people) {
        log.error(people.getName() + "---" + people.getInfo());
        return people;
    }

    //不转义
    @PostMapping("open/xssFilter")
    public String openXssFilter(String name) {
        return name;
    }

    //不转义2
    @PostMapping("open2/xssFilter")
    public String open2XssFilter(String name) {
        return name;
    }
}

json方式


image.png

键值对方式


image.png

项目地址

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

请关注我的订阅号

订阅号.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345