XSS和SQL注入是Web应用中常见计算机安全漏洞,文章主要分享通过Spring Cloud Gateway 全局过滤器对XSS和SQL注入进行安全防范。
使用版本
- spring-cloud-dependencies Hoxton.SR7
- spring-boot-dependencies 2.2.9.RELEASE
- spring-cloud-gateway 2.2.4.RELEASE
核心技术点
- AddRequestParameterGatewayFilterFactory 获取get请求参数并添加参数然后重构get请求
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI uri = exchange.getRequest().getURI();
StringBuilder query = new StringBuilder();
//获取请求url携带的参数,?号后面参数体,类似cl=3&tn=baidutop10&fr=top1000&wd=31
String originalQuery = uri.getRawQuery();
if (StringUtils.hasText(originalQuery)) {
query.append(originalQuery);
if (originalQuery.charAt(originalQuery.length() - 1) != '&') {
query.append('&');
}
}
String value = ServerWebExchangeUtils.expand(exchange, config.getValue());
query.append(config.getName());
query.append('=');
query.append(value);
try {
//重构请求uri
URI newUri = UriComponentsBuilder.fromUri(uri).replaceQuery(query.toString()).build(true).toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
return chain.filter(exchange.mutate().request(request).build());
} catch (RuntimeException var9) {
throw new IllegalStateException("Invalid URI query: \"" + query.toString() + "\"");
}
}
public String toString() {
return GatewayToStringStyler.filterToStringCreator(AddRequestParameterGatewayFilterFactory.this).append(config.getName(), config.getValue()).toString();
}
};
}
- Spring Cloud Gateway中RequestBody只能获取一次的问题解决方案
- Spring Gateway GlobalFilter
技术实现
- 创建Filter 实现GlobalFilter, Ordered
@Slf4j
@Component
public class SqLinjectionFilter implements GlobalFilter, Ordered {
@SneakyThrows
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain){
// grab configuration from Config object
log.debug("----自定义防XSS攻击网关全局过滤器生效----");
ServerHttpRequest serverHttpRequest = exchange.getRequest();
HttpMethod method = serverHttpRequest.getMethod();
String contentType = serverHttpRequest.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
URI uri = exchange.getRequest().getURI();
Boolean postFlag = (method == HttpMethod.POST || method == HttpMethod.PUT) &&
(MediaType.APPLICATION_FORM_URLENCODED_VALUE.equalsIgnoreCase(contentType) || MediaType.APPLICATION_JSON_VALUE.equals(contentType));
//过滤get请求
if (method == HttpMethod.GET) {
String rawQuery = uri.getRawQuery();
if (StringUtils.isBlank(rawQuery)){
return chain.filter(exchange);
}
log.debug("原请求参数为:{}", rawQuery);
// 执行XSS清理
rawQuery = XssCleanRuleUtils.xssGetClean(rawQuery);
log.debug("修改后参数为:{}", rawQuery);
// 如果存在sql注入,直接拦截请求
if (rawQuery.contains("forbid")) {
log.error("请求【" + uri.getRawPath() + uri.getRawQuery() + "】参数中包含不允许sql的关键词, 请求拒绝");
return setUnauthorizedResponse(exchange);
}
try {
//重新构造get request
URI newUri = UriComponentsBuilder.fromUri(uri)
.replaceQuery(rawQuery)
.build(true)
.toUri();
ServerHttpRequest request = exchange.getRequest().mutate()
.uri(newUri).build();
return chain.filter(exchange.mutate().request(request).build());
} catch (Exception e) {
log.error("get请求清理xss攻击异常", e);
throw new IllegalStateException("Invalid URI query: \"" + rawQuery + "\"");
}
}
//post请求时,如果是文件上传之类的请求,不修改请求消息体
else if (postFlag){
return DataBufferUtils.join(serverHttpRequest.getBody()).flatMap(d -> Mono.just(Optional.of(d))).defaultIfEmpty(
Optional.empty())
.flatMap(optional -> {
// 取出body中的参数
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 = serverHttpRequest.getHeaders();
// 执行XSS清理
log.debug("{} - [{}:{}] XSS处理前参数:{}", method, uri.getPath(), bodyString);
bodyString = XssCleanRuleUtils.xssPostClean(bodyString);
log.info("{} - [{}:{}] XSS处理后参数:{}", method, uri.getPath(), bodyString);
// 如果存在sql注入,直接拦截请求
if (bodyString.contains("forbid")) {
log.error("{} - [{}:{}] 参数:{}, 包含不允许sql的关键词,请求拒绝", method, uri.getPath(), bodyString);
return setUnauthorizedResponse(exchange);
}
ServerHttpRequest newRequest = serverHttpRequest.mutate().uri(uri).build();
// 重新构造body
byte[] newBytes = bodyString.getBytes(StandardCharsets.UTF_8);
DataBuffer bodyDataBuffer = toDataBuffer(newBytes);
Flux bodyFlux = Flux.just(bodyDataBuffer);
// 重新构造header
HttpHeaders headers = new HttpHeaders();
headers.putAll(httpHeaders);
// 由于修改了传递参数,需要重新设置CONTENT_LENGTH,长度是字节长度,不是字符串长度
int length = newBytes.length;
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.setContentLength(length);
headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf8");
// 重写ServerHttpRequestDecorator,修改了body和header,重写getBody和getHeaders方法
newRequest = new ServerHttpRequestDecorator(newRequest) {
@Override
public Flux getBody() {
return bodyFlux;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
};
return chain.filter(exchange.mutate().request(newRequest).build());
});
} else {
return chain.filter(exchange);
}
}
// 自定义过滤器执行的顺序,数值越大越靠后执行,越小就越先执行
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
/**
* 设置403拦截状态
*/
private Mono setUnauthorizedResponse(ServerWebExchange exchange) {
return WebfluxResponseUtil.responseFailed(exchange, HttpStatus.FORBIDDEN.value(),
"request is forbidden, SQL keywords are not allowed in the parameters.");
}
/**
* 字节数组转DataBuffer
*
* @param bytes 字节数组
* @return DataBuffer
*/
private DataBuffer toDataBuffer(byte[] bytes) {
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
}
- 定义xss注入、sql注入工具类
@Slf4j
public class XssCleanRuleUtils {
private final static Pattern[] scriptPatterns = {
Pattern.compile("", Pattern.CASE_INSENSITIVE),
Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("", Pattern.CASE_INSENSITIVE),
Pattern.compile("