赞
踩
前端某功能模块入参携带包含HTML标签的字符串,提交到后端报错:
JSON parse error: Unexpected character ('>' (code 62)): was expecting either '*' or '/' for a comment; nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('>' (code 62)): was expecting either '*' or '/' for a comment\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 3, column: 473]
前端入参
{
"content": "<p>南朝时期(公元420年至589年):九江地区成为南朝的楚州之一,属于南宋、齐、梁的管辖范围。</p><p><br></p><p>唐朝时期(公元618年至907年):九江地区属于唐朝的吉州,成为交通要地和商贸心。</p><p><br></p><p>宋、元、明、清时期(公元960年至191年):九江地区历经宋代南唐、宋朝,元朝、明朝、清朝的统治。九江成为江西省重要的政治、经济、文化中心之一。<img src=\"http://47.96.173.203:9000/lu-yi-fa/2024/01/08/100j0u000000j1zf01511_R_1600_10000_20240108142420A002.jpg\"></p>" ,
"coverImage": "",
"id": null,
"status": "1",
"title": "test",
"type": "1",
}
问题原因:
若依框架在网关采用配置拦截器的方式来处理XSS攻击,一旦请求被过滤器拦截,就会转入到自定义的拦截器XssFilter当中,首先解决的就是判断是否启用XSS拦截器和是否需要拦截(默认是开启状态),若依这里是采用在配置文件当中填写具体信息的方式,来配置是否启用xss,是否是白名单,是否是匹配链接。按照后台填写的数据处理请求,如果是不启用或者是该请求为白名单,就直接将请求放过如果不通过就交给XssFilter来处理。
XssFilter里面就是一些核心代码,就是获取请求参数,就是进行一些html标签的过滤和json字符串的处理。如下:
public class XssFilter implements GlobalFilter, Ordered
{
// 跨站脚本的 xss 配置,nacos自行添加
@Autowired
private XssProperties xss;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
ServerHttpRequest request = exchange.getRequest();
// xss开关未开启 或 通过nacos关闭,不过滤
if (!xss.getEnabled())
{
return chain.filter(exchange);
}
// GET DELETE 不过滤
HttpMethod method = request.getMethod();
if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE)
{
return chain.filter(exchange);
}
// 非json类型,不过滤
if (!isJsonRequest(exchange))
{
return chain.filter(exchange);
}
// excludeUrls 不过滤
String url = request.getURI().getPath();
if (StringUtils.matches(url, xss.getExcludeUrls()))
{
return chain.filter(exchange);
}
ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
return chain.filter(exchange.mutate().request(httpRequestDecorator).build());
}
private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange)
{
ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest())
{
@Override
public Flux<DataBuffer> getBody()
{
Flux<DataBuffer> body = super.getBody();
return body.buffer().map(dataBuffers -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
String bodyStr = new String(content, StandardCharsets.UTF_8);
// 防xss攻击过滤
bodyStr = EscapeUtil.clean(bodyStr);
// 转成字节
byte[] bytes = bodyStr.getBytes(StandardCharsets.UTF_8);
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
});
}
@Override
public HttpHeaders getHeaders()
{
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
// 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
return httpHeaders;
}
};
return serverHttpRequestDecorator;
}
/**
* 是否是Json请求
*
* @param exchange HTTP请求
*/
public boolean isJsonRequest(ServerWebExchange exchange)
{
String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
}
@Override
public int getOrder()
{
return -100;
}
}
解决方案:
在网关配置Xss过滤白名单接口
security:
# 防止XSS攻击
xss:
enabled: true
excludeUrls:
- /system/notice
- /flowable/definition/save
- /test/article/add
- /test/article/edit
当然也有其它解决方案
利用算法进行加密和解密,及前端将对应的表单数据加密,后端解密,该方法最安全。
一、前端安装crypto-js 插件,编写工具类
import CryptoJS from 'crypto-js'
// 需要和后端一致
const KEY = CryptoJS.enc.Utf8.parse('abcdefj123456');
const IV = CryptoJS.enc.Utf8.parse('abcdefj123456');
export default {
/**
* 加密
* @param {*} word
* @param {*} keyStr
* @param {*} ivStr
*/
encrypt (word, keyStr, ivStr) {
let key = KEY;
let iv = IV;
if (keyStr) {
key = CryptoJS.enc.Utf8.parse(keyStr);
iv = CryptoJS.enc.Utf8.parse(ivStr);
}
let srcs = CryptoJS.enc.Utf8.parse(word);
var encrypted = CryptoJS.AES.encrypt(srcs, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
});
return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
},
/**
* 解密
* @param {*} word
* @param {*} keyStr
* @param {*} ivStr
*/
decrypt (word, keyStr, ivStr) {
let key = KEY;
let iv = IV;
if (keyStr) {
key = CryptoJS.enc.Utf8.parse(keyStr);
iv = CryptoJS.enc.Utf8.parse(ivStr);
}
let base64 = CryptoJS.enc.Base64.parse(word);
let src = CryptoJS.enc.Base64.stringify(base64);
let decrypt = CryptoJS.AES.decrypt(src, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
});
let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr.toString();
}
}
二、表单提交前将数据加密
this.model.content = asc.encrypt(this.model.content);
三、后端接收数据并解密
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.6.0</version>
</dependency>
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* @author root
*/
public class SecretUtil {
/***
* key和iv值需要和前端一致
*/
public static final String KEY = "abcdefj123456";
public static final String IV = "abcdefj123456";
/**
* 加密方法
*
* @param data 要加密的数据
* @param key 加密key
* @param iv 加密iv
* @return 加密的结果
*/
public static String encrypt(String data, String key, String iv) {
try {
//"算法/模式/补码方式"NoPadding PkcsPadding
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
int blockSize = cipher.getBlockSize();
byte[] dataBytes = data.getBytes();
int plaintextLength = dataBytes.length;
if (plaintextLength % blockSize != 0) {
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
}
byte[] plaintext = new byte[plaintextLength];
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(plaintext);
return new Base64().encodeToString(encrypted);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 解密方法
*
* @param data 要解密的数据
* @param key 解密key
* @param iv 解密iv
* @return 解密的结果
*/
public static String desEncrypt(String data, String key, String iv) {
try {
byte[] encrypted1 = new Base64().decode(data);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] original = cipher.doFinal(encrypted1);
return new String(original).trim();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
//将content内容解密
model.setContent(SecreUtil.desEncrypt(model.getContent(),SecreUtil.Key,SecreUtil.IV));
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。