Spring Cloud Gateway网关XSS过滤方式

网友投稿 971 2022-12-02

Spring Cloud Gateway网关XSS过滤方式

Spring Cloud Gateway网关XSS过滤方式

目录1.创建一个Filter2. 处理XSS字符串3.其它使用到的工具修改

XSS是一种经常出现在web应用中的计算机安全漏洞,具体信息请自行Google。本文只分享在Spring Cloud Gateway中执行通用的XSS防范。首次作文,全是代码,若有遗漏不明之处,请各位看官原谅指点。

使用版本

Spring Cloud版本为 Greenwich.SR4

Spring Boot版本为 2.1.11.RELEASE

1.创建一个Filter

特别注意的是在处理完成之后需要重新构造请求,否则后续业务无法获得参数。

import io-ty.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-.URI;

import java.nio.charset.StandardCharsets;

import java.util.List;

import java.util.Optional;

/**

* XSS过滤

*

* @author lieber

*/

@Component

@Slf4j

@ConfigurationProperties("config.xss")

@Data

public class XssFilter implements GlobalFilter, Ordered {

private List whiteUrls;

@Override

public Mono 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);

}

// 只拦截POST和PUT请求

if ((HttpMethod.POST.name().equals(method) || HttpMethod.PUT.name().equals(method))) {

return DataBufferUtils.join(request.getBody())

.flatMap(dataBuffer -> {

// 取出body中的参数

byte[] oldBytes = new byte[dataBuffer.readableByteCount()];

dataBuffer.read(oldBytes);

String bodyString = new String(oldBytes, StandardCharsets.UTF_8);

log.debug("原请求参数为:{}", bodyString);

// 执行XSS清理

bodyString = XssUtil.INSTANCE.cleanXss(bodyString);

log.debug("修改后参数为:{}", bodyString);

ServerHttpRequest newRequest = request.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(request.getHeaders());

// 由于修改了传递参数,需要重新设置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);

}

}

/**

* 是否是白名单

*

* @param url 路由

* @param method 请求方式

* @return true/false

*/

private boolean white(String url, String method) {

return whiteUrls != null && whiteUrls.contains(XssWhiteUrl.builder().url(url).method(method).build());

}

/**

* 字节数组转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;

}

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. 处理XSS字符串

这里大范围采用Jsoup处理,然后根据自己的业务做了一部分定制。较为特殊的是,我们将字符串中含有''标示为这段文本是富文本。在清除xss攻击字符串方法时优化空间较大。

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拦截工具类

*

* @author lieber

*/

public enum XssUtil {

/**

* 实例

*/

INSTANCE;

private final static String RICH_TEXT = "";

/**

* 自定义白名单

*/

private final static Whitelist CUSTOM_WHITELIST = Whitelist.relaxed()

.addAttributes("video", "width", "height", "controls", "alt", "src")

.addAttributes(":all", "style", "class");

/**

* jsoup不格式化代码

*/

private final static Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);

/**

* 清除json对象中的xss攻击字符

*

* @param val json对象字符串

* @return 清除后的json对象字符串

*/

private String cleanObj(String val) {

JSONObject jsonObject = JSONObject.parseObject(val);

for (Map.Entry 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();

}

/**

* 清除json数组中的xss攻击字符

*

* @param val json数组字符串

* @return 清除后的json数组字符串

*/

private String cleanArr(String val) {

List list = JSONObject.parseArray(val, String.class);

List result = new ArrayList<>(list.size());

for (String str : list) {

str = this.cleanXss(str);

result.add(str);

}

return JSONObject.toJSONString(result);

}

/**

* 清除xss攻击字符串,此处优化空间较大

*

* @param str 字符串

* @return 清除后无害的字符串

*/

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, "参数包含特殊字符");

}

str = richText ? afterClean : this.backSpecialStr(afterClean);

}

return str;

}

/**

* 判断是否是富文本

*

* @param str 待判断字符串

* @return true/false

*/

private boolean richText(String str) {

return str.contains(RICH_TEXT);

}

/**

* 判断是否参数错误

*

* @param richText 是否富文本

* @param afterClean 清理后字符

* @param str 原字符串

* @return true/false

*/

private boolean paramError(boolean richText, String afterClean, String str) {

// 如果包含富文本字符,那么不是参数错误

if (richText) {

return false;

}

// 如果清理后的字符和清理前的字符匹配,那么不是参数错误

if (Objects.equals(str, afterClean)) {

return false;

}

// 如果仅仅包含可以通过的特殊字符,那么不是参数错误

if (Objects.equals(str, this.backSpecialStr(afterClean))) {

return false;

}

// 如果还有......

return true;

}

/**

* 转义回特殊字符

*

* @param str 已经通过转义字符

* @return 转义后特殊字符

*/

private String backSpecialStr(String str) {

return str.replaceAll("&", "&");

}

}

3.其它使用到的工具

import com.alibaba.fastjson.JSONObject;

import org.springframework.util.StringUtils;

/**

* JSON处理工具类

*

* @author lieber

*/

public enum JsonUtil {

/**

* 实例

*/

INSTANCE;

/**

* json对象字符串开始标记

*/

private final static String JSON_OBJECT_START = "{";

/**

* json对象字符串结束标记

*/

private final static String JSON_OBJECT_END = "}";

/**

* json数组字符串开始标记

*/

private final static String JSON_ARRAY_START = "[";

/**

* json数组字符串结束标记

*/

private final static String JSON_ARRAY_END = "]";

/**

* 判断字符串是否json对象字符串

*

* @param val 字符串

* @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;

}

/**

* 判断字符串是否json数组字符串

*

* @param val 字符串

* @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;

}

/**

* 判断对象是否是json对象

*

* @param obj 待判断对象

* @return true/false

*/

public boolean isJsonObj(Object obj) {

String str = JSONObject.toJSONString(obj);

return this.isJsonObj(str);

}

/**

* 判断字符串是否json字符串

*

* @param str 字符串

* @return true/false

*/

public boolean isJson(String str) {

if (StringUtils.isEmpty(str)) {

return false;

}

return this.isJsonObj(str) || this.isJsonArr(str);

}

}

大功告成。

----------------手动分隔----------------

修改

感谢@chang_p_x的指正,在第一步创建Filter时有问题,原因是使用了新旧代码的问题,现已经将元代码放在正文,新代码如下

@Override

public Mono 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 -> {

// 取出body中的参数

String bodyString = "";

if (optional.isPresent()) {

byte[] oldBytes = new byte[optional.get().readableByteCount()];

optional.get().read(oldBytes);

bodyString = new StBJEZuKFVHgring(oldBytes, StandardCharsets.UTF_8);

}

HttpHeaders httpHeaders = request.getHeaders();

// 执行XSS清理

log.debug("{} - [{}:{}] XSS处理前参数:{}", method, uri.getPath(), bodyString);

bodyString = XssUtil.INSTANCE.cleanXss(bodyString);

log.info("{} - [{}:{}] 参数:{}", method, uri.getPath(), bodyString);

BJEZuKFVHg ServerHttpRequest newRequest = request.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);

}

}

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:.NET 读取 JSON格式的数据
下一篇:C#添加删除IIS的主机头
相关文章

 发表评论

暂时没有评论,来抢沙发吧~