Learn springboot from scratch and implement an Xss Filter

Posted by webing on Sun, 19 Dec 2021 03:06:32 +0100

preface

Project security requires xss filtering of global parameters

Introduction to Xss

Many people may know about Xss. Out of "Politeness",

Salted fish gentleman or simply take an example

Users can fill in their names when registering

At this point, I filled in "" and submitted it,

On the back end, it was saved without any detection

Then there may be a problem. Next time you visit this page, you will find a pop-up window "1"!

There are many kinds of Xss attacks. Those who are interested can consult the data by themselves. I won't say more here

So how to face Xss attack?

In fact, it is also very easy to solve. In a word, "don't trust any input of users"!

Programming is to filter the content submitted by users and "escape" illegal characters!

Implementation of Springboot Xss interceptor

Filtering and escaping the submission of the whole system, such as calling a method for each point to do this, will be very troublesome, and the repeated code looks ugly. Therefore, we use Springboot+Filter to implement an Xss global filter

Springboot implements an Xss filter in two common ways:

  • Override HttpServletRequestWrapper
    Rewrite getHeader(), getParameter(), getParameterValues(), getInputStream() to filter the traditional "key value pair" parameter transfer methods
    Rewrite getInputStream() to filter the parameters passed in Json mode, that is, the @ RequestBody parameter

  • Customize the serializer and set the objectMapper of MappingJackson2HttpMessageConverter
    Override jsonserializer Serialize() to filter the output parameters (PS: save the data as it is and escape it when it is taken out)
    Override jsondeserializer Deserialize() to filter the input parameters (PS: save after data escape)

There are several precautions for the above two methods:

Problem 1: processing of json parameter (@ RequestBody)

For parameter transfer in Json mode,
Spring MVC uses Jack JSON for serialization by default
When you override getInputStream() to implement xss filtering,
If you replace the double quotation marks for the parameters, an error will be reported when the jackjson sequence / deserialization parameters,
Because it doesn't know this format!
Therefore, we should eliminate double quotation marks when processing parameters!
Alternatively, you can choose a custom serializer to handle json parameters

Problem 2: Custom serializer

Although we have written cases in both ways,
In fact, you only need to choose one of them! There is no need to filter both
It is recommended to handle the input parameters for the following reasons:

  1. Overriding getHeader(), getParameter(), and getParameterValues() directly escapes the input parameters,
    That is, the parameters obtained by your subsequent programs are escaped,
    Therefore, for global unification, we also input parameters of @ RequestBody type

  2. Escaping the input parameter means that the data stored in the DB is "safe"

The implementation principle of this case is as follows:

  • Override HttpServletRequestWrapper filtering for key value pair parameters
  • Use a custom serializer to filter json parameters

PS: getInputStream() filtering is also implemented for json parameters (the code is in annotation state)

realization

First, let's introduce a toolkit, the famous "muddle headed" toolkit, which integrates a large number of easy-to-use tools! Highly recommended!

<!--HuTool tool kit -->
<dependency>
     <groupId>cn.hutool</groupId>
     <artifactId>hutool-all</artifactId>
     <version>5.2.3</version>
</dependency>

We use escape util in HuTool to escape special characters

Use HttpServletRequestWrapper to override Request parameters

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 Filtering treatment
 */
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    /**
     * Description: constructor
     *
     * @param request Request object
     */
    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }


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

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

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

//    //Rewrite getInputStream to filter json format parameters (that is, parameters of @ RequestBody type)
//    @Override
//    public ServletInputStream getInputStream() throws IOException {
//        //Non json type, return directly
//        if (!super.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
//            return super.getInputStream();
//        }
//
//        //Null, return directly
//        String json = IoUtil.read(super.getInputStream(), "utf-8");
//        if (StrUtil.isEmpty(json)) {
//            return super.getInputStream();
//        }
//        //It should be noted here that parameters in json format cannot directly use the escape util. Of hutool Escape, because it will escape "also,
//        //This makes @ RequestBody unable to resolve into a normal object, so we implement a filtering method ourselves
//        //Or you can customize your own objectMapper to handle the escape of json access parameters (recommended)
//        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;
//    }

}

Implement a filter

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;

/**
 * Filters to prevent XSS attacks
 */
@Component
public class XssFilter implements Filter {
    /**
     * Exclude links
     */
    private List<String> excludes = new ArrayList<>();

    /**
     * xss Filter switch
     */
    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);
    }

    /**
     * Judge whether the current path needs to be filtered
     */
    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;
    }
}

Configure and register filters

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 to configure
 */
@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());
        //Add filter path
        registration.addUrlPatterns(StrUtil.split(urlPatterns, ","));
        registration.setName("xssFilter");
        registration.setOrder(Integer.MAX_VALUE);
        //Set initialization parameters
        Map<String, String> initParameters = new HashMap<String, String>();
        initParameters.put("excludes", excludes);
        initParameters.put("enabled", enabled);
        registration.setInitParameters(initParameters);
        return registration;
    }

    /**
     * Filter json type
     *
     * @param builder
     * @return
     */
    @Bean
    @Primary
    public ObjectMapper xssObjectMapper(Jackson2ObjectMapperBuilder builder) {
        //Parser
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        //Register xss parser
        SimpleModule xssModule = new SimpleModule("XssStringJsonDeserializer");

        //Just select one of the input and output parameters for filtering. There is no need to add both
        //In order to unify with XssHttpServletRequestWrapper, it is recommended to handle the input parameters
        //Register parameter escape
        xssModule.addDeserializer(String.class, new XssStringJsonDeserializer());
        //Register parameter escape
        //xssModule.addSerializer(new XssStringJsonSerializer());
        objectMapper.registerModule(xssModule);
        //return
        return objectMapper;
    }
}

Finally, we add

#Open
xss.enabled=true
#Unfiltered paths, separated by commas
xss.excludes=/open/*,/open2/*
//Filter path, comma separated
xss.urlPatterns=/*

In addition, we need to implement an ObjectMapper to handle json format parameters

Handle the escape of json input parameters

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;

/**
 * Handle the escape of json input parameters
 */
public class XssStringJsonDeserializer extends JsonDeserializer<String> {

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

    //Escape of incoming parameter
    @Override
    public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        String value = jsonParser.getText();
        if (value != null) {
            return EscapeUtil.escape(value);
        }
        return value;
    }

}

Handle the escape of json parameters

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;

/**
 * Handle the escape of json parameters
 */
public class XssStringJsonSerializer extends JsonSerializer<String> {

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

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

}

Write a test

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 {

    //Key value pair
    @PostMapping("xssFilter")
    public String xssFilter(String name, String info) {
        log.error(name + "---" + info);
        return name + "---" + info;
    }
    //entity
    @PostMapping("modelXssFilter")
    public People modelXssFilter(@RequestBody People people) {
        log.error(people.getName() + "---" + people.getInfo());
        return people;
    }

    //No escape
    @PostMapping("open/xssFilter")
    public String openXssFilter(String name) {
        return name;
    }

    //No escape 2
    @PostMapping("open2/xssFilter")
    public String open2XssFilter(String name) {
        return name;
    }
}

json mode

Key value pair method

Please follow my subscription number

Topics: Java Spring Boot