Spring Cloud Gateway Gateway XSS Filtering Mode

  • 2021-11-24 01:43:57
  • OfStack

Directory 1. Create 1 Filter 2. Handle XSS string 3. Modify other tools used

XSS is a computer security vulnerability that often appears in web applications. Please refer to Google for specific information. This article only shares the common XSS prevention implemented in Spring Cloud Gateway. For the first time, the composition is full of codes. If there are any omissions and ambiguities, please forgive and give directions.

Use version

Spring Cloud version is Greenwich. SR4 Spring Boot version 2.1. 11. RELEASE

1. Create an Filter

Particular attention is paid to the need to reconstruct the request after the processing is complete, otherwise the subsequent business cannot get the parameters.


import io.netty.buffer.ByteBufAllocator;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.DigestUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.validation.constraints.NotEmpty;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
/**
 * XSS Filter 
 *
 * @author lieber
 */
@Component
@Slf4j
@ConfigurationProperties("config.xss")
@Data
public class XssFilter implements GlobalFilter, Ordered {
    private List<XssWhiteUrl> whiteUrls;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        URI uri = request.getURI();
        String method = request.getMethodValue();
        //  Determine whether it is in the white list 
        if (this.white(uri.getPath(), method)) {
            return chain.filter(exchange);
        }
        //  Intercept only POST And PUT Request 
        if ((HttpMethod.POST.name().equals(method) || HttpMethod.PUT.name().equals(method))) {
            return DataBufferUtils.join(request.getBody())
                    .flatMap(dataBuffer -> {
                        //  Take out body Parameters in 
                        byte[] oldBytes = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(oldBytes);
                        String bodyString = new String(oldBytes, StandardCharsets.UTF_8);
                        log.debug(" The original request parameters are: {}", bodyString);
                        //  Execute XSS Clean up 
                        bodyString = XssUtil.INSTANCE.cleanXss(bodyString);
                        log.debug(" The modified parameters are: {}", bodyString);
                        ServerHttpRequest newRequest = request.mutate().uri(uri).build();
                        //  Reconstruct body
                        byte[] newBytes = bodyString.getBytes(StandardCharsets.UTF_8);
                        DataBuffer bodyDataBuffer = toDataBuffer(newBytes);
                        Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
                        //  Reconstruct header
                        HttpHeaders headers = new HttpHeaders();
                        headers.putAll(request.getHeaders());
                        //  Because the delivery parameters have been modified, you need to reset the CONTENT_LENGTH The length is byte length, not string length 
                        int length = newBytes.length;
                        headers.remove(HttpHeaders.CONTENT_LENGTH);
                        headers.setContentLength(length);
                        headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf8");
                        //  Rewrite ServerHttpRequestDecorator , modified body And header , rewrite getBody And getHeaders Method 
                        newRequest = new ServerHttpRequestDecorator(newRequest) {
                            @Override
                            public Flux<DataBuffer> getBody() {
                                return bodyFlux;
                            }
                            @Override
                            public HttpHeaders getHeaders() {
                                return headers;
                            }
                        };
                        return chain.filter(exchange.mutate().request(newRequest).build());
                    });
        } else {
            return chain.filter(exchange);
        }
    }
    /**
     *  Whether it is a white list 
     *
     * @param url     Route 
     * @param method  Request mode 
     * @return true/false
     */
    private boolean white(String url, String method) {
        return whiteUrls != null && whiteUrls.contains(XssWhiteUrl.builder().url(url).method(method).build());
    }
    /**
     *  Byte array conversion DataBuffer
     *
     * @param bytes  Byte array 
     * @return DataBuffer
     */
    private DataBuffer toDataBuffer(byte[] bytes) {
        NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
        DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
        buffer.write(bytes);
        return buffer;
    }
    public static final int ORDER = 10;
    @Override
    public int getOrder() {
        return ORDER;
    }
    @Data
    @Validated
    @AllArgsConstructor
    @NoArgsConstructor
    private static class XssWhiteUrl {
        @NotEmpty
        private String url;
        @NotEmpty
        private String method;
    }
}

2. Working with XSS strings

Jsoup is widely used here, and then one part of customization is made according to one's own business. More specifically, we put the string with ' < /'Mark this text as rich text. The optimization space is large when clearing the xss attack string method.


import com.alibaba.fastjson.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Whitelist;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
 * xss Interceptor tool class 
 *
 * @author lieber
 */
public enum XssUtil {
    /**
     *  Instances 
     */
    INSTANCE;
    private final static String RICH_TEXT = "</";
    /**
     *  Custom Whitelist 
     */
    private final static Whitelist CUSTOM_WHITELIST = Whitelist.relaxed()
            .addAttributes("video", "width", "height", "controls", "alt", "src")
            .addAttributes(":all", "style", "class");
    /**
     * jsoup Unformatted code 
     */
    private final static Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
    /**
     *  Clear json Object in the xss Attack character 
     *
     * @param val json Object string 
     * @return  Cleaned json Object string 
     */
    private String cleanObj(String val) {
        JSONObject jsonObject = JSONObject.parseObject(val);
        for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
            if (entry.getValue() != null && entry.getValue() instanceof String) {
                String str = (String) entry.getValue();
                str = this.cleanXss(str);
                entry.setValue(str);
            }
        }
        return jsonObject.toJSONString();
    }
    /**
     *  Clear json In the array xss Attack character 
     *
     * @param val json Array string 
     * @return  Cleaned json Array string 
     */
    private String cleanArr(String val) {
        List<String> list = JSONObject.parseArray(val, String.class);
        List<String> result = new ArrayList<>(list.size());
        for (String str : list) {
            str = this.cleanXss(str);
            result.add(str);
        }
        return JSONObject.toJSONString(result);
    }
    /**
     *  Clear xss Attack string, where optimization space is large 
     *
     * @param str  String 
     * @return  Harmless string after clearing 
     */
    public String cleanXss(String str) {
        if (JsonUtil.INSTANCE.isJsonObj(str)) {
            str = this.cleanObj(str);
        } else if (JsonUtil.INSTANCE.isJsonArr(str)) {
            str = this.cleanArr(str);
        } else {
            boolean richText = this.richText(str);
            if (!richText) {
                str = str.trim();
                str = str.replaceAll(" +", " ");
            }
            String afterClean = Jsoup.clean(str, "", CUSTOM_WHITELIST, OUTPUT_SETTINGS);
            if (paramError(richText, afterClean, str)) {
                throw new BizRunTimeException(ApiCode.PARAM_ERROR, " Parameter contains special characters ");
            }
            str = richText ? afterClean : this.backSpecialStr(afterClean);
        }
        return str;
    }
    /**
     *  Determine whether it is rich text 
     *
     * @param str  String to be judged 
     * @return true/false
     */
    private boolean richText(String str) {
        return str.contains(RICH_TEXT);
    }
    /**
     *  Determine whether the parameter is wrong 
     *
     * @param richText    Rich text or not 
     * @param afterClean  Cleaned characters 
     * @param str         Original string 
     * @return true/false
     */
    private boolean paramError(boolean richText, String afterClean, String str) {
        //  If rich text characters are included, it is not a parameter error 
        if (richText) {
            return false;
        }
        //  If the character after cleaning matches the character before cleaning, it is not a parameter error 
        if (Objects.equals(str, afterClean)) {
            return false;
        }
        //  If you only include special characters that can pass, then it is not a parameter error 
        if (Objects.equals(str, this.backSpecialStr(afterClean))) {
            return false;
        }
        //  If there is any more ......
        return true;
    }
    /**
     *  Escape back to special characters 
     *
     * @param str  Escaped characters have been passed 
     * @return  Special character after escape 
     */
    private String backSpecialStr(String str) {
        return str.replaceAll("&amp;", "&");
    }
}

3. Other tools used


import com.alibaba.fastjson.JSONObject;
import org.springframework.util.StringUtils;
/**
 * JSON Processing tool class 
 *
 * @author lieber
 */
public enum JsonUtil {
    /**
     *  Instances 
     */
    INSTANCE;
    /**
     * json Object string opening tag 
     */
    private final static String JSON_OBJECT_START = "{";
    /**
     * json Object string closing tag 
     */
    private final static String JSON_OBJECT_END = "}";
    /**
     * json Array string opening tag 
     */
    private final static String JSON_ARRAY_START = "[";
    /**
     * json Array string closing tag 
     */
    private final static String JSON_ARRAY_END = "]";
    /**
     *  Determining whether a string json Object string 
     *
     * @param val  String 
     * @return true/false
     */
    public boolean isJsonObj(String val) {
        if (StringUtils.isEmpty(val)) {
            return false;
        }
        val = val.trim();
        if (val.startsWith(JSON_OBJECT_START) && val.endsWith(JSON_OBJECT_END)) {
            try {
                JSONObject.parseObject(val);
                return true;
            } catch (Exception e) {
                return false;
            }
        }
        return false;
    }
    /**
     *  Determining whether a string json Array string 
     *
     * @param val  String 
     * @return true/false
     */
    public boolean isJsonArr(String val) {
        if (StringUtils.isEmpty(val)) {
            return false;
        }
        val = val.trim();
        if (StringUtils.isEmpty(val)) {
            return false;
        }
        val = val.trim();
        if (val.startsWith(JSON_ARRAY_START) && val.endsWith(JSON_ARRAY_END)) {
            try {
                JSONObject.parseArray(val);
                return true;
            } catch (Exception e) {
                return false;
            }
        }
        return false;
    }
    /**
     *  Determining whether the object is json Object 
     *
     * @param obj  Object to be judged 
     * @return true/false
     */
    public boolean isJsonObj(Object obj) {
        String str = JSONObject.toJSONString(obj);
        return this.isJsonObj(str);
    }
    /**
     *  Determining whether a string json String 
     *
     * @param str  String 
     * @return true/false
     */
    public boolean isJson(String str) {
        if (StringUtils.isEmpty(str)) {
            return false;
        }
        return this.isJsonObj(str) || this.isJsonArr(str);
    }
}

Be crowned with success.

----Manual segregation----

Modify

Thanks for the @ chang_p_x correction, there was a problem in creating Filter in Step 1, due to the problem of using old and new code. Now the meta code has been put in the body, and the new code is as follows


@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        URI uri = request.getURI();
        String method = request.getMethodValue();
        if (this.white(uri.getPath(), method)) {
            return chain.filter(exchange);
        }
        if ((HttpMethod.POST.name().equals(method) || HttpMethod.PUT.name().equals(method))) {
            return DataBufferUtils.join(request.getBody()).flatMap(d -> Mono.just(Optional.of(d))).defaultIfEmpty(Optional.empty())
                    .flatMap(optional -> {
                        //  Take out body Parameters in 
                        String bodyString = "";
                        if (optional.isPresent()) {
                            byte[] oldBytes = new byte[optional.get().readableByteCount()];
                            optional.get().read(oldBytes);
                            bodyString = new String(oldBytes, StandardCharsets.UTF_8);
                        }
                        HttpHeaders httpHeaders = request.getHeaders();
                        //  Execute XSS Clean up 
                        log.debug("{} - [{} : {}] XSS Pre-processing parameters: {}", method, uri.getPath(), bodyString);
                        bodyString = XssUtil.INSTANCE.cleanXss(bodyString);
                        log.info("{} - [{} : {}]  Parameters: {}", method, uri.getPath(), bodyString);
                        
                        ServerHttpRequest newRequest = request.mutate().uri(uri).build();
                        //  Reconstruct body
                        byte[] newBytes = bodyString.getBytes(StandardCharsets.UTF_8);
                        DataBuffer bodyDataBuffer = toDataBuffer(newBytes);
                        Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
                        //  Reconstruct header
                        HttpHeaders headers = new HttpHeaders();
                        headers.putAll(httpHeaders);
                        //  Because the delivery parameters have been modified, you need to reset the CONTENT_LENGTH The length is byte length, not string length 
                        int length = newBytes.length;
                        headers.remove(HttpHeaders.CONTENT_LENGTH);
                        headers.setContentLength(length);
                        headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf8");
                        //  Rewrite ServerHttpRequestDecorator , modified body And header , rewrite getBody And getHeaders Method 
                        newRequest = new ServerHttpRequestDecorator(newRequest) {
                            @Override
                            public Flux<DataBuffer> getBody() {
                                return bodyFlux;
                            }
                            @Override
                            public HttpHeaders getHeaders() {
                                return headers;
                            }
                        };
                        return chain.filter(exchange.mutate().request(newRequest).build());
                    });
        } else {
            return chain.filter(exchange);
        }
    }

Related articles: