SpringCloud gateway 防止XSS漏洞
一.XSS(跨站脚本)漏洞详解
1.XSS的原理和分类
跨站脚本攻击XSS(Cross Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。XSS攻击针对的是用户层面的攻击!
XSS分为:存储型 、反射型 、DOM型XSS
-
存储型XSS:存储型XSS,持久化,代码是存储在服务器中的,如在个人信息或发表文章等地方,插入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,用户访问该页面的时候触发代码执行。这种XSS比较危险,容易造成蠕虫,盗窃cookie
-
反射型XSS:非持久化,需要欺骗用户自己去点击链接才能触发XSS代码(服务器中没有这样的页面和内容),一般容易出现在搜索页面
-
DOM型XSS:不经过后端,DOM-XSS漏洞是基于文档对象模型(Document Objeet Model,DOM)的一种漏洞,DOM-XSS是通过url传入参数去控制触发的,其实也属于反射型XSS。
2.XSS漏洞的危害
从以上我们可以知道,存储型的XSS危害最大。因为他存储在服务器端,所以不需要我们和被攻击者有任何接触,只要被攻击者访问了该页面就会遭受攻击。而反射型和DOM型的XSS则需要我们去诱使用户点击我们构造的恶意的URL,需要我们和用户有直接或者间接的接触,比如利用社会工程学或者利用在其他网页挂马的方式。
3.XSS的防御
XSS防御的总体思路是:对用户的输入(和URL参数)进行过滤,对输出进行html编码。也就是对用户提交的所有内容进行过滤,对url中的参数进行过滤,过滤掉会导致脚本执行的相关内容;然后对动态输出到页面的内容进行html编码,使脚本无法在浏览器中执行。
对输入的内容进行过滤,可以分为黑名单过滤和白名单过滤。黑名单过滤虽然可以拦截大部分的XSS攻击,但是还是存在被绕过的风险。白名单过滤虽然可以基本杜绝XSS攻击,但是真实环境中一般是不能进行如此严格的白名单过滤的。
对输出进行html编码,就是通过函数,将用户的输入的数据进行html编码,使其不能作为脚本运行。
如下,是使用php中的htmlspecialchars函数对用户输入的name参数进行html编码,将其转换为html实体
#使用htmlspecialchars函数对用户输入的name参数进行html编码,将其转换为html实体
$name = htmlspecialchars( $_GET[ 'name' ] );
二.Java开发中防范XSS跨站脚本攻击的思路
-
- 防堵跨站漏洞
阻止攻击者利用在被攻击网站上发布跨站攻击语句不可以信任用户提交的任何内容,首先代码里对用户输入的地方和变量都需要仔细检查长度和对”<”,”>”,”;”,”’”等字符做过滤;其次任何内容写到页面之前都必须加以encode,避免不小心把html tag 弄出来。这一个层面做好,至少可以堵住超过一半的XSS 攻击。
- 防堵跨站漏洞
-
- Cookie 防盗
首先避免直接在cookie 中泄露用户隐私,例如email、密码等等。其次通过使cookie 和系统ip 绑定来降低cookie 泄露后的危险。这样攻击者得到的cookie 没有实际价值,不可能拿来重放。
- Cookie 防盗
-
- 尽量采用POST 而非GET 提交表单
POST 操作不可能绕开javascript 的使用,这会给攻击者增加难度,减少可利用的跨站漏洞。
- 尽量采用POST 而非GET 提交表单
-
- 严格检查refer
检查http refer 是否来自预料中的url。这可以阻止第2 类攻击手法发起的http 请求,也能防止大部分第1 类攻击手法,除非正好在特权操作的引用页上种了跨站访问。
- 严格检查refer
-
- 将单步流程改为多步,在多步流程中引入效验码
多步流程中每一步都产生一个验证码作为hidden 表单元素嵌在中间页面,下一步操作时这个验证码被提交到服务器,服务器检查这个验证码是否匹配。
首先这为第1 类攻击者大大增加了麻烦。其次攻击者必须在多步流程中拿到上一步产生的效验码才有可能发起下一步请求,这在第2 类攻击中是几乎无法做到的。
- 将单步流程改为多步,在多步流程中引入效验码
-
- 引入用户交互
简单的一个看图识数可以堵住几乎所有的非预期特权操作。
- 引入用户交互
-
- 只在允许anonymous 访问的地方使用动态的javascript。
-
- 对于用户提交信息的中的img 等link,检查是否有重定向回本站、不是真的图片等可疑操作。
-
- 内部管理网站的问题
很多时候,内部管理网站往往疏于关注安全问题,只是简单的限制访问来源。这种网站往往对XSS 攻击毫无抵抗力,需要多加注意。安全问题需要长期的关注,从来不是一锤子买卖。XSS 攻击相对其他攻击手段更加隐蔽和多变,和业务流程、代码实现都有关系,不存在什么一劳永逸的解决方案。此外,面对XSS,往往要牺牲产品的便利性才能保证完全的安全,如何在安全和便利之间平衡也是一件需要考虑的事情。
web应用开发者注意事项:
- 1.对于开发者,首先应该把精力放到对所有用户提交内容进行可靠的输入验证上。这些提交内容包括URL、查询关键字、http头、post数据等。只接受在你所规定长度范围内、采用适当格式、你所希望的字符。阻塞、过滤或者忽略其它的任何东西。
- 2.保护所有敏感的功能,以防被bots自动化或者被第三方网站所执行。实现session标记(session tokens)、CAPTCHA系统或者HTTP引用头检查。
- 3.如果你的web应用必须支持用户提供的HTML,那么应用的安全性将受到灾难性的下滑。但是你还是可以做一些事来保护web站点:确认你接收的HTML内容被妥善地格式化,仅包含最小化的、安全的tag(绝对没有JavaScript),去掉任何对远程内容的引用(尤其是样式表和JavaScript)。为了更多的安全,请使用httpOnly的cookie。
三.相关代码(适用于spring cloud gateway)
代码执行顺序
CacheBodyGlobalFilter—>XssRequestGlobalFilter—>XssResponseGlobalFilter
1.CacheBodyGlobalFilter.java
这个过滤器解决body不能重复读的问题(在低版本的spring-cloud不需要这个过滤器),为后续的XssRequestGlobalFilter重写请求body做准备
package com.xxx.gateway.filter;
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.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @Author:
* @Description: 这个过滤器解决body不能重复读的问题,为后续的XssRequestGlobalFilter重写post|put请求的body做准备
* @Date:
* <p>
* 没把body的内容放到attribute中去,因为从attribute取出body内容还是需要强转成 Flux<DataBuffer>,然后转换成String,和直接读取body没有什么区别
*/
@Component
public class CacheBodyGlobalFilter implements Ordered, GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
HttpMethod method = exchange.getRequest().getMethod();
String contentType = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
if (method == HttpMethod.POST || method == HttpMethod.PUT) {
if (MediaType.APPLICATION_FORM_URLENCODED_VALUE.equalsIgnoreCase(contentType)
|| MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(contentType)
|| MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType)) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
DataBufferUtils.retain(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux
.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
2.XssRequestGlobalFilter.java
自定义防XSS攻击网关全局过滤器。
package com.xxx.gateway.filter;
import io.netty.buffer.ByteBufAllocator;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
/**
* @Author:
* @Description: 自定义防XSS攻击网关全局过滤器
* @Date:
*/
@Component
public class XssRequestGlobalFilter implements GlobalFilter, Ordered {
private Logger logger = LoggerFactory.getLogger(XssRequestGlobalFilter.class);
/**
*
* @param exchange
* @param chain
* @return
*
* get请求参考spring cloud gateway自带过滤器:
* @see org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory
*
* post请求参考spring cloud gateway自带过滤器:
* @see org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){
// grab configuration from Config object
logger.info("----自定义防XSS攻击网关全局过滤器生效----");
String path = exchange.getRequest().getPath().toString();
ServerHttpRequest serverHttpRequest = exchange.getRequest();
HttpMethod method = serverHttpRequest.getMethod();
String contentType = serverHttpRequest.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
Boolean postFlag = (method == HttpMethod.POST || method == HttpMethod.PUT) &&
(MediaType.APPLICATION_FORM_URLENCODED_VALUE.equalsIgnoreCase(contentType) || MediaType.APPLICATION_JSON_VALUE.equals(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType));
// get 请求, 参考的是 org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory
if (method == HttpMethod.GET) {
URI uri = exchange.getRequest().getURI();
String rawQuery = uri.getRawQuery();
if (StringUtils.isBlank(rawQuery)){
return chain.filter(exchange);
}
rawQuery = XssCleanRuleUtils.xssClean(rawQuery);
try {
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) {
logger.error("get请求清理xss攻击异常", e);
throw new IllegalStateException("Invalid URI query: \"" + rawQuery + "\"");
}
}
//post请求时,如果是文件上传之类的请求,不修改请求消息体
else if (postFlag){
// 参考的是 org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory
//从请求里获取Post请求体
String bodyStr = resolveBodyFromRequest(serverHttpRequest);
// 这种处理方式,必须保证post请求时,原始post表单必须有数据过来,不然会报错
if (StringUtils.isBlank(bodyStr)) {
logger.error("请求异常:{} POST请求必须传递参数", serverHttpRequest.getURI().getRawPath());
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.BAD_REQUEST);
byte[] bytes = "{\"code\":400,\"msg\":\"post data error\"}".getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}
//白名单处理(看业务需求)
boolean containsTarget = WhiteListUtils.richTextUrls.stream().anyMatch(s -> path.contains(s));
if (containsTarget) {
//bodyStr = XssCleanRuleUtils.xssRichTextClean(bodyStr);
bodyStr = XssCleanRuleUtils.xssClean2(bodyStr);
} else {
bodyStr = XssCleanRuleUtils.xssClean(bodyStr);
}
URI uri = serverHttpRequest.getURI();
URI newUri = UriComponentsBuilder.fromUri(uri).build(true).toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
// 定义新的消息头
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// 由于修改了传递参数,需要重新设置CONTENT_LENGTH,长度是字节长度,不是字符串长度
int length = bodyStr.getBytes().length;
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.setContentLength(length);
// 设置CONTENT_TYPE
if (StringUtils.isNotBlank(contentType)) {
headers.set(HttpHeaders.CONTENT_TYPE, contentType);
}
// 由于post的body只能订阅一次,由于上面代码中已经订阅过一次body。所以要再次封装请求到request才行,不然会报错请求已经订阅过
request = new ServerHttpRequestDecorator(request) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
// this causes a 'HTTP/1.1 411 Length Required' on httpbin.org
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};
//封装request,传给下一级
request.mutate().header(HttpHeaders.CONTENT_LENGTH, Integer.toString(bodyStr.length()));
return chain.filter(exchange.mutate().request(request).build());
} else {
return chain.filter(exchange);
}
}
@Override
public int getOrder() {
return -90;
}
/**
* 从Flux<DataBuffer>中获取字符串的方法
* @return 请求体
*/
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
//获取请求体
Flux<DataBuffer> body = serverHttpRequest.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
//获取request body
return bodyRef.get();
}
/**
* 字符串转DataBuffer
* @param value
* @return
*/
private DataBuffer stringBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
}
3.XssResponseGlobalFilter.java
重写Response,防止xss攻击。
package com.xxx.gateway.filter;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
/**
* @Author:
* @Description: 重写Response,防止xss攻击
* @Date:
*/
@Component
public class XssResponseGlobalFilter implements Ordered, GlobalFilter {
private Logger logger = LoggerFactory.getLogger(XssResponseGlobalFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求url
String path = exchange.getRequest().getPath().toString();
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
String contentType = getDelegate().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
Boolean flag = MediaType.APPLICATION_JSON_VALUE.equals(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType);
if (body instanceof Flux && flag) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
//如果响应过大,会进行截断,出现乱码,
//然后看api DefaultDataBufferFactory有个join方法可以合并所有的流,乱码的问题解决
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = null;
try {
join = dataBufferFactory.join(dataBuffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
//释放掉内存
DataBufferUtils.release(join);
String result = new String(content, Charset.forName("UTF-8"));
//logger.info("result:"+result);
//若为带有富文本的接口,走富文本xss过滤
boolean containsTarget = WhiteListUtils.richTextUrls.stream().anyMatch(s -> path.contains(s));
if (containsTarget) {
//result = XssCleanRuleUtils.xssRichTextClean(result);
result = XssCleanRuleUtils.xssClean2(result);
} else {
//result就是response的值,对result进行去XSS
result = XssCleanRuleUtils.xssClean(result);
}
byte[] uppedContent = new String(result.getBytes(), Charset.forName("UTF-8")).getBytes();
return bufferFactory.wrap(uppedContent);
} catch (Exception e) {
// 处理异常,记录日志等
throw e;
} finally {
if (join != null) {
//释放掉内存
DataBufferUtils.release(join);
}
}
}));
}
// if body is not a flux. never got there.
return super.writeWith(body);
}
};
// replace response with decorator
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return -50;
}
}
4.XssCleanRuleUtils.java (工具类)
上面几个过滤器使用的工具类
后续我自己这边没有用Jsoup了(感觉有坑,就放弃了)
package com.xxx.gateway.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Whitelist;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.regex.Pattern;
/**
* @Author:
* @Description: xss过滤工具
* @Date:
*/
public class XssCleanRuleUtils {
//xss过滤规则(对于script、src及加载事件和弹窗事件的代码块)
private final static Pattern[] scriptPatterns = {
Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
};
//非富文本的
public static String xssClean(String value) {
if (value != null) {
value = value.replaceAll("\0|\n|\r", "");
for (Pattern pattern : scriptPatterns) {
value = pattern.matcher(value).replaceAll("");
}
value = value.replaceAll("<", "<").replaceAll(">", ">");
}
return value;
}
//富文本的
public static String xssClean2(String value) {
if (value != null) {
value = value.replaceAll("\0|\n|\r", "");
for (Pattern pattern : scriptPatterns) {
value = pattern.matcher(value).replaceAll("");
}
}
return value;
}
//自定义的json白名单
private static final ClassPathResource jsoupWhiteListPathRes = new ClassPathResource("/json/xssWhiteList.json");
//配置过滤化参数, 不对代码进行格式化
private static final Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false);
//富文本的(使用了Jsoup)
public static String xssRichTextClean(String value) {
// 创建一个自定义的白名单,基于Jsoup的默认白名单
Whitelist customWhitelist = Whitelist.basic();
InputStream whiteConfig = null;
try {
whiteConfig = jsoupWhiteListPathRes.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
if (whiteConfig == null) {
throw new RuntimeException("读取jsoup xss 白名单文件失败");
} else {
try {
JSONObject whiteListJson = JSON.parseObject(whiteConfig, JSONObject.class);
//添加标签 addTags
JSONArray addTagsJsonArr = whiteListJson.getJSONArray("addTags");
String[] addTagsArr = addTagsJsonArr.toArray(new String[0]);
customWhitelist.addTags(addTagsArr);
//添加属性 addAttributes
JSONArray addAttrJsonArr = whiteListJson.getJSONArray("addAttributes");
Iterator<Object> iter = addAttrJsonArr.iterator();
while (iter.hasNext()) {
JSONObject attrJsonObj = (JSONObject) iter.next();
String tag = attrJsonObj.getString("tag");
JSONArray attrJsonArr = attrJsonObj.getJSONArray("attributes");
String[] attrArr = attrJsonArr.toArray(new String[0]);
customWhitelist.addAttributes(tag, attrArr);
}
//添加 addProtocols
JSONArray addProtoJsonArr = whiteListJson.getJSONArray("addProtocols");
iter = addProtoJsonArr.iterator();
while (iter.hasNext()) {
JSONObject attrJsonObj = (JSONObject) iter.next();
String tag = attrJsonObj.getString("tag");
String attribute = attrJsonObj.getString("attribute");
JSONArray protoJsonArr = attrJsonObj.getJSONArray("protocols");
String[] protocolArr = protoJsonArr.toArray(new String[0]);
customWhitelist.addProtocols(tag, attribute, protocolArr);
}
} catch (IOException e) {
e.printStackTrace();
}
}
value =Jsoup.clean(value, "", customWhitelist, outputSettings);
return value;
}
}
5.WhiteListUtils(白名单)
package com.xxx.gateway.filter;
import java.util.Arrays;
import java.util.List;
//白名单
public class WhiteListUtils {
//白名单接口
public static final List<String> richTextUrls = Arrays.asList(
"/xxx/information/findByUnid",
"/xxx/getNoticeAnnouncementDetail");
}
6.xssWhiteList.json (自定义的Jsoup白名单)
转载:
{
"addTags": [
"p",
"strong",
"em",
"u",
"s",
"blockquote",
"pre",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"br",
"ol",
"li",
"ul",
"sub",
"sup",
"span",
"iframe",
"a",
"img"
],
"addAttributes": [
{
"tag": ":all",
"attributes": [
"class",
"id",
"src",
"style"
]
},
{
"tag": "pre",
"attributes": [
"spellcheck"
]
},
{
"tag": "iframe",
"attributes": [
"frameborder",
"allowfullscreen",
"src"
]
},
{
"tag": "a",
"attributes": [
"rel",
"target"
]
},
{
"tag": "img",
"attributes": [
"align",
"alt",
"height",
"src",
"title",
"width"
]
}
],
"addProtocols": [
{
"tag": "xx",
"attribute": "src",
"protocols": [
"src",
"http",
"https",
"data"
]
}
]
}
7.注意启动类不要忘记加@ComponentScan
四.Jsoup
Jsoup 使用标签 ** 白名单 ** 的机制用来进行防止 XSS 攻击,
假设白名单中只允许 p 标签存在, 此时在一段 HTML 代码中,** 只能存在 p 标签 **,
其他标签将会被清除,只保留被标签所包裹的内容.
转载:https://www.jianshu.com/p/32abc12a175a
-
项目依赖
-
jsoup-1.9.2
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.9.2</version>
</dependency>
jsoup 默认给我们提供了 5 个白名单对象
白名单对象 | 标签 | 说明 |
---|---|---|
none | 无 | 只保留标签内文本内容 |
simpleText | b,em,i,strong,u | 简单的文本标签 |
basic | a,b,blockquote,br,cite,code,dd,dl,dt,em,i,li,ol, p,pre,q,small,span,strike,strong,sub,sup,u,ul | 基本使用的标签 |
basicWithImages | basic 的基础上添加了 img 标签 及 img 标签的 src,align,alt,height,width,title 属性 | 基本使用的加上 img 标签 |
relaxed | a,b,blockquote,br,caption,cite,code,col,colgroup,dd, div,dl,dt,em,h1,h2,h3,h4,h5,h6,i,img,li,ol,p,pre,q,small, span,strike,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,u,ul | 在 basicWithImages 的基础上又增加了一部分部分标签 |
如果没有图片上传的需求, 使用 basic
, 否则使用 basicWithImages
- 其他事项
在刚才测试的时候, 会发现 Jsoup.clean() 方法返回的代码已经被进行格式化, 在标签及标签内容之间添加了 \n 回车符, 如果不需要的话, 可以使用Jsoup.clean(testHtml, "", whitelist, new Document.OutputSettings().prettyPrint(false));
进行过滤
** 以下为实际项目中所使用的工具类 **
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Whitelist;
/**
* 描述: 过滤 HTML 标签中 XSS 代码
*/
public class JsoupUtil {
/**
* 使用自带的 basicWithImages 白名单
* 允许的便签有 a,b,blockquote,br,cite,code,dd,dl,dt,em,i,li,ol,p,pre,q,small,span,strike,strong,sub,sup,u,ul,img
* 以及 a 标签的 href,img 标签的 src,align,alt,height,width,title 属性
*/
private static final Whitelist whitelist = Whitelist.basicWithImages();
/** 配置过滤化参数, 不对代码进行格式化 */
private static final Document.OutputSettings outputSettings = new Document.OutputSettings().prettyPrint(false);
static {
// 富文本编辑时一些样式是使用 style 来进行实现的
// 比如红色字体 style="color:red;"
// 所以需要给所有标签添加 style 属性
whitelist.addAttributes(":all", "style");
}
public static String clean(String content) {
return Jsoup.clean(content, "", whitelist, outputSettings);
}
}
五.参考文章
【1】spring cloud gateway 过滤器防止跨站脚本攻击(存储XSS、反射XSS)
https://blog.csdn.net/qq_26801767/article/details/106235359?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-4-106235359-blog-128300551.235v40pc_relevant_3m_sort_dl_base1&spm=1001.2101.3001.4242.3&utm_relevant_index=7
【2】XSS(跨站脚本)漏洞详解之XSS跨站脚本攻击漏洞的解决
http://www.uml.org.cn/safe/202203034.asp
【3】SpringCloud微服务实战——搭建企业级开发框架(五十一):微服务安全加固—自定义Gateway拦截器实现防止SQL注入/XSS攻击
https://blog.csdn.net/wmz1932/article/details/129449794
【4】Spring Cloud Gateway 实现XSS、SQL注入拦截
https://www.jianshu.com/p/17613323463d
【5】SpringBoot针对富文本和非富文本添加xss过滤
https://blog.csdn.net/ChOLg/article/details/119949942
【6】详解Xss 及SpringBoot 防范Xss攻击(附全部代码)
https://www.cnblogs.com/blbl-blog/p/17188558.html