工作总结之安全问题篇
工作总结之安全问题篇
前言
谷歌好帮手啊,百度不太行,寻找各种能解决问题的方法。
appscan的工作原理
这里大致讲一下这边摸索出的appscan的识别原理,网上讲的都不明白,实践是检验真理的唯一标准。
1.会根据接口返回的状态码来判断它的攻击是否有效:200 有效
2.会根据返回内容是否含它自己注入的攻击脚本(例如script标签)来判断它的攻击是否有效,含有,则有效(所以这也解释了为什么过滤器将这些字符替换掉也能被判定为攻击无效的现象)
解决方法
主要思路就是通过过滤器和拦截器去识别敏感字符(路径或者是传参),要么替换掉敏感字符,要么直接拦截给出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("");
// 避免 onload=
scriptPattern = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
return value;
}
}
拦截器(写在preHandle方法中)
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;
}
}
}
拦截器的私有方法
private boolean judgeSQLInject(String value){
if (null == value || "".equals(value)){
return false;
}
String sqlStr = "and|or|select|insert|update|delete|drop|truncate|alert|eval|1=1";
String[] sqlArr = sqlStr.split("\\|");
for (String s : sqlArr) {
if (value.contains(s)){
return true;
}
}
return false;
}
private Boolean stopXss(String value) {
String[] regexArr = {"<(\\s)*(no)?script", "(no)?script(\\s)*>", "<(\\s)*iframe", "iframe(\\s)*>", "<(\\s)*img",
"img(\\s)*>", "src(\\s)*=", "<(\\s)*a", "a(\\s)*>", "href(\\s)*=",
"style(\\s)*=", "function\\(", "eval\\(", "expression\\(", "javascript:",
"vbscript:", "view-source:", "window[\\s\\S]*location", "window\\.", "\\.location",
"document[\\s\\S]*cookie", "document\\.", "alert\\(", ":alert", "window[\\s\\S]*open",
"oncontrolselect", "oncopy", "oncut", "ondataavailable", "ondatasetchanged",
"ondatasetcomplete", "ondblclick", "ondeactivate", "ondrag", "ondragend",
"ondragenter", "ondragleave", "ondragover", "ondragstart", "ondrop",
"onerror", "onerroupdate", "onfilterchange", "onfinish", "onfocus",
"onfocusin", "onfocusout", "onhelp", "onkeydown", "onkeypress",
"onkeyup", "onlayoutcomplete", "onload", "onlosecapture", "onmousedown",
"onmouseenter", "onmouseleave", "onmousemove", "onmousout", "onmouseover",
"onmouseup", "onmousewheel", "onmove", "onmoveend", "onmovestart",
"onabort", "onactivate", "onafterprint", "onafterupdate", "onbefore",
"onbeforeactivate", "onbeforecopy", "onbeforecut", "onbeforedeactivate", "onbeforeeditocus",
"onbeforepaste", "onbeforeprint", "onbeforeunload", "onbeforeupdate", "onblur",
"onbounce", "oncellchange", "onchange", "onclick", "oncontextmenu",
"onpaste", "onpropertychange", "onreadystatechange", "onreset", "onresize",
"onresizend", "onresizestart", "onrowenter", "onrowexit", "onrowsdelete",
"onrowsinserted", "onscroll", "onselect", "onselectionchange", "onselectstart",
"onstart", "onstop", "onsubmit", "onunload", "select",
"insert", "delete", "from", "count\\(", "drop table",
"update", "truncate", "asc\\(", "mid\\(", "char\\(",
"xp_cmdshell", "exec", "master", "netlocalgroup administrators", "net user",
"or", "and", "\\+", "'", "\"","\\.\\."};
for (String regex : regexArr) {
//匹配完整单词,避免误伤
if (Pattern.matches("\\w+", regex.trim())) {
regex = "\\b"+regex.trim()+"\\b";
}
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(value);
if (matcher.find()) {
return Boolean.TRUE;
}
}
return Boolean.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(之前好像有看到过相关的文章,应该是的,可以再深入查一查)
总结
这样能解决解决大部分的安全问题,例如:跨站伪造、钓鱼、Unix问题、sql盲注等等