一 背景
一些业务支持用户输入一些信息,而输入的信息可以是一些javascript脚本之类的,然后在页面显示时也未做好处理,导致该脚本会在页面直接执行,这就是所谓的xss攻击。
为了避免这些这种情况,这些接口可能需要防止xss脚本的传入,对于使用了SpringCloudGateway的网关那该如何做呢。
也许我们会想,在每个需要控制的接口里,做一下xss的控制。但是,涉及到非常多的接口,可能会非常麻烦,任何改动或者优化都需要动到这么多的接口,所以不合适这么做。
我们可以自定义一个路由过滤器,指定哪些uri需要去拦截请求并把请求入参或body里包含的所有xss脚本全部去掉或者改掉或者直接报错,以达到从接口层面控制xss脚本传入的问题。
二 具体实现代码
xss脚本替换工具类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46import org.springframework.util.StringUtils;
import java.util.regex.Pattern;
public final class XSSUtils {
private XSSUtils() {
}
private static final Pattern[] PATTERNS = {
// Avoid anything in a <script> type of expression
Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
// Avoid anything in a src='...' type of expression
Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
// Remove any lonesome </script> tag
Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
// Avoid anything in a <iframe> type of expression
Pattern.compile("<iframe>(.*?)</iframe>", Pattern.CASE_INSENSITIVE),
// Remove any lonesome <script ...> tag
Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
// Remove any lonesome <img ...> tag
Pattern.compile("<img(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
// Avoid eval(...) expressions
Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
// Avoid expression(...) expressions
Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
// Avoid javascript:... expressions
Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
// Avoid vbscript:... expressions
Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
// Avoid onload= expressions
Pattern.compile("on(load|error|mouseover|submit|reset|focus|click)(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
};
public static String stripXSS(String value) {
if (StringUtils.isEmpty(value)) {
return value;
}
for (Pattern scriptPattern : PATTERNS) {
value = scriptPattern.matcher(value).replaceAll("");
}
return value;
}
}
自定义路由过滤器改请求参数与body1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 需要过滤xss的uri正则过滤
*
* @author chenxiaoqi
* @since 2019/08/14
*/
4j
public class XssUriRegexGatewayFilterFactory extends AbstractGatewayFilterFactory<XssUriRegexGatewayFilterFactory.Config> {
private static final Set<HttpMethod> SUPPORT_METHODS = new HashSet<>(Arrays.asList(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH));
/**
* Regex key.
*/
public static final String REGEX_KEY = "regex";
private final List<HttpMessageReader<?>> messageReaders;
public XssUriRegexGatewayFilterFactory() {
super(XssUriRegexGatewayFilterFactory.Config.class);
this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
}
public List<String> shortcutFieldOrder() {
return Collections.singletonList(REGEX_KEY);
}
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
HttpMethod method = serverHttpRequest.getMethod();
String contentType = serverHttpRequest.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
// 不支持的方法
if (!SUPPORT_METHODS.contains(method)) {
return chain.filter(exchange);
}
// 校验uri是否符合
String regex = config.getRegex();
String uri = serverHttpRequest.getURI().getPath();
if (!uri.matches(regex)) {
return chain.filter(exchange);
}
// contentType 为空不处理
if (StringUtils.isEmpty(contentType)) {
return chain.filter(exchange);
}
String lowerContentType = contentType.toLowerCase();
// contentType 必须是参数或文本类型
if (!MediaType.APPLICATION_FORM_URLENCODED_VALUE.equalsIgnoreCase(contentType)
&& !lowerContentType.contains("json") && !lowerContentType.contains("text") && !lowerContentType.contains("xml")) {
return chain.filter(exchange);
}
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(serverHttpRequest.getQueryParams().size());
for (Map.Entry<String, List<String>> entry : serverHttpRequest.getQueryParams().entrySet()) {
queryParams.addAll(entry.getKey(), entry.getValue().stream().map(XSSUtils::stripXSS).collect(Collectors.toList()));
}
// 参考api文档中GatewapFilter中“修改请求消息体拦截器”:ModifyRequestBodyGatewayFilterFactory.java
ServerRequest serverRequest = ServerRequest.create(exchange, this.messageReaders);
Mono<String> rawBody = serverRequest.bodyToMono(String.class).map(XSSUtils::stripXSS);
BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(rawBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// the new content type will be computed by bodyInserter
// and then set in the request decorator
headers.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers, queryParams);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
ServerHttpRequest decorator = decorate(exchange, headers,
outputMessage);
return chain.filter(exchange.mutate().request(decorator).build());
}));
};
}
ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
// TODO: this causes a 'HTTP/1.1 411 Length Required' // on
// httpbin.org
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
public MultiValueMap<String, String> getQueryParams() {
return outputMessage.getQueryParams();
}
};
}
public static class Config {
private String regex;
}
}
用于缓存body的类(由于requestBody只能读取一次)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84import org.reactivestreams.Publisher;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.function.Supplier;
/**
* Implementation of {@link ClientHttpRequest} that saves body as a field.
*/
class CachedBodyOutputMessage implements ReactiveHttpOutputMessage {
private final DataBufferFactory bufferFactory;
private final HttpHeaders httpHeaders;
private final MultiValueMap<String, String> queryParams;
private Flux<DataBuffer> body = Flux.error(new IllegalStateException(
"The body is not set. " + "Did handling complete with success?"));
CachedBodyOutputMessage(ServerWebExchange exchange, HttpHeaders httpHeaders, MultiValueMap<String, String> queryParams) {
this.bufferFactory = exchange.getResponse().bufferFactory();
this.httpHeaders = httpHeaders;
this.queryParams = queryParams;
}
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
}
public boolean isCommitted() {
return false;
}
public HttpHeaders getHeaders() {
return this.httpHeaders;
}
public DataBufferFactory bufferFactory() {
return this.bufferFactory;
}
/**
* Return the request body, or an error stream if the body was never set or when.
*
* @return body as {@link Flux}
*/
public Flux<DataBuffer> getBody() {
return this.body;
}
public MultiValueMap<String, String> getQueryParams() {
return this.queryParams;
}
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
this.body = Flux.from(body);
return Mono.empty();
}
public Mono<Void> writeAndFlushWith(
Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMap(p -> p));
}
public Mono<Void> setComplete() {
return writeWith(Flux.empty());
}
}
初始化bean1
2
3
4
5
6
7
8
9/**
* xss脚本过滤
*
* @return
*/
public XssUriRegexGatewayFilterFactory xssUriRegexGatewayFilterFactory() {
return new XssUriRegexGatewayFilterFactory();
}
三 在网关中的使用
在网关中的配置示例如下:
针对全部uri都做了替换和校验,正常情况下我们可能不需要全部都支持,调整XssUriRegex
正则即可。1
2
3
4
5
6
7
8routes:
- id: xxx-service
uri: http://xxx-service:8080
predicates:
- Path=/xxx/**
filters:
- RewritePath=/xxx(?<segment>.*), /api$\{segment}
- XssUriRegex=^.*$