Spring Cloud Gateway Gateway XSS Filtering Mode
- 2021-11-24 01:43:57
- OfStack
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("&", "&");
}
}
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);
}
}