📢 叮咚,现场运维来消息了,说项目被检测到有高危漏洞,要求修复,以为就是jar安全漏洞,升级就完事了,就让发过来看看👀,亚麻袋住了,"XSS检测绕过(UTF-7编码绕过)",从没见过啊,还是UTF-7。


怎么搞?我电脑上的编辑器都没找到有支持UTF-7编码的,首先想到的,把这些信息丢给DeepSeek帮我分析看看,问Ai怎么防御?结果没有我想要的方案。
然后去网络搜索下吧,看看大家前辈们有没解决过,果然有相关文件,但是都没给出具体解决方案,不过也有所收获,得到了一段UTF-7编码的XSS注入参数(如果Get参数请求,记得对参数URL编码)
+ADw-script+AD4-alert('UTF-7 XSS')+ADw-/script+AD4-
进入正题,结合项目代码,想到可以用Filter过滤器对参数拦截,那就动手来吧,以项目SpringCloud Zuul为例
yaml
# xss regex
xss:
enable: true
regexes:
# UTF-7编码绕过
- "(?i)(\\+[A-Za-z0-9+/=,]+\\-|\\+(?:ADw|AD4|AC8|ACI|AHs|AH0)-)"
java
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil;
import com.framework.util.RegexUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import javax.annotation.PostConstruct;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Data
@Slf4j
@Component
@Configuration
@EqualsAndHashCode(callSuper = true)
@ConfigurationProperties(prefix = "xss")
@ConditionalOnProperty(prefix = "xss", name = "enable", havingValue = "true")
public class XssZuulFilter extends ZuulFilter {
/**
* 正则
*/
private List<String> regexes = new ArrayList<>();
private static List<Pattern> injectionPatterns = new ArrayList<>();
@PostConstruct
public void init() {
log.debug("XssZuulFilter#init-regexes: {}", this.regexes);
this.regexes.forEach(regex -> {
injectionPatterns.add(
Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
);
});
}
@Override
public String filterType() {
return "pre"; // 在路由之前执行
}
@Override
public int filterOrder() {
return 0; // 高优先级执行
}
@Override
public boolean shouldFilter() {
return true; // 对所有请求进行过滤
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String requestURI = request.getRequestURI();
// 排除ureport预览时"过期"报错
if (!RegexUtil.pathAnyMatch(requestURI, "/api-config/ureport/designer/savePreviewData")) {
// 1. 检查URL参数
checkUrlParameters(request, ctx);
// 2. 检查请求体
if (isJsonRequest(request)) {
checkJsonRequestBody(request, ctx);
}
}
return null;
}
/**
* 检查请求参数
*
* @param request HttpServletRequest
* @param ctx RequestContext
*/
private void checkUrlParameters(HttpServletRequest request, RequestContext ctx) {
Map<String, String[]> params = request.getParameterMap();
for (Map.Entry<String, String[]> entry : params.entrySet()) {
for (String value : entry.getValue()) {
String result = containsInjection(value);
if (result != null) {
log.debug("XssZuulFilter#checkUrlParameters-result: {}", result);
blockRequest(ctx, String.format("请求参数包含敏感内容: 『%s』,请修正后再次请求。", result));
return;
}
}
}
}
/**
* 检查请求JSON体
*
* @param request HttpServletRequest
* @param ctx RequestContext
*/
private void checkJsonRequestBody(HttpServletRequest request, RequestContext ctx) {
try (InputStream in = request.getInputStream()) {
if (in != null) {
String result = null;
String body = StreamUtils.copyToString(in, StandardCharsets.UTF_8);
log.debug("XssZuulFilter#checkJsonRequestBody-body: {}", body);
// 先解析JSON,然后检查每个值
if (StrUtil.isNotEmpty(body)) {
if (JSONUtil.isJson(body)) {
JSON json = JSONUtil.parse(body);
log.debug("XssZuulFilter#checkJsonRequestBody-json: {}", json);
result = checkJsonObject(json);
} else { // 不是有效JSON,直接检查原始内容
result = containsInjection(body);
}
}
if (result != null) {
log.debug("XssZuulFilter#checkJsonRequestBody-result: {}", result);
blockRequest(ctx, String.format("请求数据包含敏感内容: 『%s』,请修正后再次提交。", result));
return;
}
// 重新写入请求体
ctx.setRequest(new CustomHttpServletRequestWrapper(request, body));
}
} catch (IOException e) {
e.printStackTrace();
blockRequest(ctx, "敏感内容检查失败: " + e.getMessage());
}
}
/**
* 检查json体
*
* @param json
* @return
*/
private String checkJsonObject(Object json) {
String result = null;
// 处理JSON对象
if (json instanceof Map) {
Map<?, ?> map = (Map<?, ?>) json;
log.debug("XssZuulFilter#checkJsonObject-map: {}", map);
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (entry.getValue() instanceof String) {
result = containsInjection((String) entry.getValue());
} else if (entry.getValue() != null) {
result = checkJsonObject(entry.getValue());
}
if (result != null) {
return result;
}
}
} else if (json instanceof List) { // 处理JSON数组
List<?> list = (List<?>) json;
log.debug("XssZuulFilter#checkJsonObject-list: {}", list);
for (Object item : list) {
if (item instanceof String) {
result = containsInjection((String) item);
} else if (item != null) {
result = checkJsonObject(item);
}
if (result != null) {
return result;
}
}
}
return result;
}
/**
* 是否JSON请求
*
* @param request HttpServletRequest
* @return true/false
*/
private boolean isJsonRequest(HttpServletRequest request) {
String contentType = request.getContentType();
return contentType != null && contentType.contains("application/json");
}
/**
* 匹配正则
*
* @param input 输入内容
* @return null / 匹配的group(0)
*/
private String containsInjection(String input) {
if (StrUtil.isEmpty(input)) {
return null;
}
// 跳过纯数字和布尔值
if (
BooleanUtil.or(
input.matches("^\\d+\\.?\\d*$"),
input.equalsIgnoreCase("true"),
input.equalsIgnoreCase("false")
)
) {
return null;
}
for (Pattern pattern : injectionPatterns) {
Matcher matcher = pattern.matcher(input);
if (matcher.find()) {
log.debug("XssZuulFilter#containsInjection-input: {}", input);
return matcher.group(0);
}
}
return null;
}
/**
* 输入消息提示
*
* @param ctx RequestContext
* @param message 消息内容
*/
private void blockRequest(RequestContext ctx, String message) {
ResponseWrapper<Boolean, ?> responseWrapper = new ResponseWrapper<>();
responseWrapper.setStatus(false)
.setMessage(message);
ctx.setSendZuulResponse(false); // 不进行路由
ctx.setResponseStatusCode(HttpStatus.OK.value()); // Bad Request
ctx.setResponseBody(JSONUtil.toJsonStr(responseWrapper));
ctx.getResponse().setContentType("application/json;charset=UTF-8");
}
/**
* 重新写入请求体 对于请求流
*/
private static class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final String body;
CustomHttpServletRequestWrapper(HttpServletRequest request, String body) {
super(request);
this.body = body;
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
return new ServletInputStream() {
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
}
写完收工,可以愉快地摸鱼了🎉!