目录
工作总结之安全问题篇
前言
对于我们公司的辣鸡项目还要跑安全测试,真是太抬举它了,谁会去攻击内网的系统?
好了,问题还是要来解决的,测试使用的软件是appscan,会出一个安全报告,告诉开发去改,报告又不说人话,开发是狗啊,开发还真的是狗,上司技术经理跟个傻逼一样,只会说大话,不干实事,瞎逼逼,还打乱自己的思路,一句很简单完事,不知道他是怎么面上来的,看完报告没有啥头绪,于是去百度,去谷歌,寻找各种能解决问题的方法。
解决方法
主要思路就是通过过滤器和拦截器去识别敏感字符(路径或者是传参),要么替换掉敏感字符,要么直接拦截给出403之类的错误码。
具体实现的代码
过滤器
import com.workplat.filter.wrapper.XssHttpRequestWrapper;
import com.workplat.utils.PropertiesUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @program: approval
* @description: 全局过滤器
* @author: xinghao
* @create: 2022-12-05 15:22
*/
public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("=====过滤器初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String xssEnabledStr = PropertiesUtils.getPropertyValue("xss.enabled");
Boolean xssEnabled = Boolean.valueOf(xssEnabledStr);
// System.out.println("过滤器非bean读取的值:" + xssEnabled);
if(xssEnabled){
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
String referer=request.getHeader("referer");
if(StringUtils.isNotBlank(referer) && !referer.contains(request.getServerName())){
/**
* 如果 链接地址来自其他网站,则返回禁止访问
*/
response.setStatus(403);
}else{
StringBuffer requestURL = request.getRequestURL();
if (findUnixChar(requestURL.toString())){
response.setStatus(403);
return ;
}
XssHttpRequestWrapper requestWrapper = new XssHttpRequestWrapper(request);
if(servletRequest instanceof HttpServletRequest){
filterChain.doFilter(requestWrapper, servletResponse);
// filterChain.doFilter(servletRequest, servletResponse);
}else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
}else {
filterChain.doFilter(servletRequest,servletResponse);
}
// System.out.println("=====过滤器走你");
}
@Override
public void destroy() {
System.out.println("=====过滤器销毁");
}
private Boolean findUnixChar(String value) {
String unixCharPattern= "\\.\\.";
Pattern pattern = Pattern.compile(unixCharPattern);
Matcher matcher = pattern.matcher(value);
if (matcher.find()){
return Boolean.TRUE;
}
return Boolean.FALSE;
}
}
请求参数的包装类
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StreamUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.util.regex.Pattern;
/**
* @program: approval
* @description: XSS攻击过滤器
* @author: xinghao
* @create: 2022-12-05 15:04
*/
@Slf4j
public class XssHttpRequestWrapper extends ContentCachingRequestWrapper {
/**
* 缓存下来的HTTP body
*/
// private final String body;
public XssHttpRequestWrapper(HttpServletRequest servletRequest) throws IOException {
super(servletRequest);
// StringBuilder stringBuilder = new StringBuilder();
// BufferedReader bufferedReader = null;
// InputStream inputStream = null;
// try {
// inputStream = servletRequest.getInputStream();
// if (inputStream != null) {
// bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// char[] charBuffer = new char[128];
// int bytesRead = -1;
// while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
// stringBuilder.append(charBuffer, 0, bytesRead);
// }
// } else {
// stringBuilder.append("");
// }
// } catch (IOException ex) {
//
// } finally {
// if (inputStream != null) {
// try {
// inputStream.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// if (bufferedReader != null) {
// try {
// bufferedReader.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }
// body = stringBuilder.toString();
}
/**
* 重新包装输入流
* @return
* @throws IOException
*/
// @Override
// public ServletInputStream getInputStream() throws IOException {
// final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
// ServletInputStream servletInputStream = new ServletInputStream() {
// @Override
// public boolean isFinished() {
// return false;
// }
//
// @Override
// public boolean isReady() {
// return false;
// }
//
// @Override
// public void setReadListener(ReadListener readListener) {
// }
//
// @Override
// public int read() throws IOException {
// return byteArrayInputStream.read();
// }
// };
// return servletInputStream;
//
// }
//
//
// @Override
// public BufferedReader getReader() throws IOException {
// return new BufferedReader(new InputStreamReader(this.getInputStream()));
// }
//
// public String getBody() {
// return this.body;
// }
@Override
public String[] getParameterValues(String parameter) {
String[] values = super.getParameterValues(parameter);
if (values == null) {
return null;
}
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = cleanXSS(values[i]);
}
return encodedValues;
}
@Override
public String getParameter(String parameter) {
String value = super.getParameter(parameter);
if (value == null) {
return null;
}
return cleanXSS(value);
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
if (value == null) return null;
return cleanXSS(value);
}
private String cleanXSS(String value) {
if (value == null || "".equals(value)){
return value;
}
value = value.replaceAll(";", "");
value = value.replaceAll("\"", "");
value = value.replaceAll("%3C/", "");
value = value.replaceAll("%3C", "");
value = value.replaceAll("%3", "");
value = value.replaceAll("%3E", "");
value = value.replaceAll("%22%3E%3C", "");
value = value.replaceAll("%2e%2e", "");
value = value.replaceAll("win.ini", "");
value = value.replaceAll("boot.ini", "");
// value = value.replaceAll("\\.\\.", "");
value = value.replaceAll("\\|", "");
//过滤过多导致登录失败
// value = value.replaceAll("&", "");
value = value.replaceAll("\\$", "");
value = value.replaceAll("%", "");
//过滤过多导致登录失败
// value = value.replaceAll("@", "");
value = value.replaceAll("\"", "");
value = value.replaceAll("\\+", "");
value = value.replaceAll(",", "");
// 避免 null 字符
value = value.replaceAll("", "");
value = value.replaceAll("<", "& lt;").replaceAll(">", "& gt;");
value = value.replaceAll("<", "& lt;").replaceAll(">", "& gt;");// 全角大于号 全角小于号
value = value.replaceAll("\\(", "& #40;").replaceAll("\\)", "& #41;");// \\( \\)
value = value.replaceAll("'", "& #39;");
// 避免 任何 script 标签
Pattern scriptPattern = Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// 避免 任何 src="..."
scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// 删除任何 </script> 标签
scriptPattern = Pattern.compile("</script>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// 删除任何 <script ...> 标签
scriptPattern = Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// 避免 eval(...)
scriptPattern = Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// 避免 expression(...)
scriptPattern = Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// 避免 javascript:...
scriptPattern = Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// 避免 vbscript:...
scriptPattern = Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// 避免 onl oad=
scriptPattern = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
return value;
}
}
拦截器(写在preHandl方法中)
String xssEnabledStr = PropertiesUtils.getPropertyValue("xss.enabled");
Boolean xssEnabled = Boolean.valueOf(xssEnabledStr);
// System.out.println("拦截器非bean读取的值:" + xssEnabled);
if(xssEnabled){
// 如果是OPTIONS请求,让其响应一个 200状态码,说明可以正常访问
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
// 放行OPTIONS请求
return true;
}
//获取get请求参数
Map<String, String[]> parameterMap = request.getParameterMap();
if (parameterMap.size() > 0){
for (String key : parameterMap.keySet()) {
String value = Arrays.toString(parameterMap.get(key));
System.out.println("===get请求参数key:" + key + ", value:" + value);
if(stopXss(value)){
response.setStatus(403);
return false;
}
}
}
//获取post请求body
byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream());
String body = new String(bodyBytes, request.getCharacterEncoding());
System.out.println("===post请求体:" + body);
if (StringUtils.isNotBlank(body) && stopXss(body)){
response.setStatus(403);
return false;
}
// 拦截攻击字符
if (StringUtils.isNotBlank(request.getQueryString())){
String s = request.getQueryString().toLowerCase();
if (judgeSQLInject(s)){
response.setContentType("text/html;charset=UTF-8");
response.getWriter().print("参数含有非法攻击字符,已禁止继续访问!");
response.setStatus(403);
return false;
}
}
}
配置读取类
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.stereotype.Component;
import org.springframework.util.StringValueResolver;
@Component
public class PropertiesUtils implements EmbeddedValueResolverAware {
private static StringValueResolver stringValueResolver;
@Override
public void setEmbeddedValueResolver(StringValueResolver stringValueResolver) {
PropertiesUtils.stringValueResolver = stringValueResolver;
}
public static String getPropertyValue(String propertyName) {
String finalPropertyName = "${"+propertyName+"}";
return PropertiesUtils.stringValueResolver.resolveStringValue(finalPropertyName);
}
}
解释一下部分不好懂的地方
String xssEnabledStr = PropertiesUtils.getPropertyValue("xss.enabled");
这是为了将是否开启安全过滤开关集成在了配置里面,因为识别的字符太多有时候会影响正常功能,配置一个开关以应付安全问题检查- XssHttpRequestWrapper为什么继承的是ContentCachingRequestWrapper而不是网上看到的大部分的都是HttpServletRequestWrapper
首先要明白,该Wrapper类有两个作用:一个是重写请求的一些基础方法,getParameter、getParameterValues等,在spring为controller方法的参数解析赋值的时候,底层其实也是调的这些方法,所以重写时就可以加上敏感字符的替换,才能起到作用;另一个作用是重写getInputStream,用于防止参数的二次使用时空指针(就是我们要解决的controller层方法参数为空指针问题),因为根据规范,getInputStream只能被调用一次,第二次就会为空,如果Wrapper类将其缓存起来就能实现多次调用了,最初重写该方法是为了解决post请求体的数据(请求体的数据只能通过getInputStream得到)不能二次读取的问题,但随着了解逐渐深入,发现getParameter这类方法的底层也是getInputStream,所以理论上,如果在过滤器或者是拦截器调了getParameter这类方法(第一次调用),而又没有重写getInputStream,那么controller层的方法参数(第二次调用)仍然会是空指针的情况。结论就是,对于想要在过滤器或者是拦截器使用请求参数的场景,是必须重写getInputStream。
再次回到实际遇到的问题,这个问题原因在于继承HttpServletRequestWrapper对于controller层的不带类似@RequestParam的参数(就是方法里对于要获取的参数前面什么注解都不写,正常情况下,spring也能自动解析赋值,但我们现在不是正常情况),会是空指针,spring解析不出来,而ContentCachingRequestWrapper是spring提供的,已经帮我们重写过了getInputStream方法,继承它就规避了这个问题。
ps:我不太确定如果加上注解是不是也能规避这个空指针问题,当时遇到这个问题的时候,方法参数前面正好没有任何注解,根据上面的分析我觉得就算加上也不能规避。
ps:我想起来了,网上都是继承HttpServletRequestWrapper并且手动重写getInputStream方法能够解决post请求体的数据二次读取的问题,但是对于我遇到的情况:不是json数据,单个参数的场景(无注解,依赖于spring自动解析赋值),是会空指针的,所以需要ContentCachingRequestWrapper
ps:我隐约记得:无注解,依赖于spring自动解析赋值,底层好像也是调的getParameter(之前好像有看到过相关的文章,应该是的,可以再深入查一查)