如果一个网站是从其他网站点击后跳转过来,则会在当前的请求头中带上Referer信息,该信息指示的是跳转前网页的地址信息,而AppScan安全扫描则会报CSRF 攻击;解决方法:通过拦截器,获取请求中的Referer信息,对Referer信息做过滤处理
1、拦截器信息
可用过csrf-ignore-uri配置白单信息,指定相应的URI信息不做拦截
package cn.com.zwjp.framework.web.filter;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cn.com.zwjp.framework.util.Configuration;
import cn.com.zwjp.framework.web.filter.util.IgnoreUriUtil;
/**
* CSRF 攻击 将可能干扰 CSRF 攻击的 HTTP 头除去,并使用伪造的 URL 设置 Referer 头
*
* @author 爱兰河少
* @data 2017年7月6日
* cn.com.zwjp.framework.web.filter
*/
public class RefererFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(RefererFilter.class);
private List<String> bdUris = null;
public void init(FilterConfig filterConfig) throws ServletException {
String ignoreUri = filterConfig.getInitParameter("csrf-ignore-uri");
if (ignoreUri != null) {
bdUris = Arrays.asList(ignoreUri.toLowerCase().trim().split(","));
}
if (bdUris == null || bdUris.size() <= 0) {
ignoreUri = Configuration.getInstance().getValue("csrf-ignore-uri");
if (StringUtils.isNotBlank(ignoreUri)) {
bdUris = Arrays.asList(ignoreUri.toLowerCase().trim().split(","));
}
}
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String rawReferer = httpServletRequest.getHeader("referer");
// referer白单处理
boolean legal = IgnoreUriUtil.isIgnoreUri(httpServletRequest);
//白单
legal=!legal?IgnoreUriUtil.isIgnoreUri(bdUris, httpServletRequest):legal;
if (!legal) {
logger.warn(" illegal referer: {}", rawReferer);
String rootCauseMessage = "可能存在跨域攻击的风险:" + rawReferer;
// 向后台打印出错信息
logger.error(rootCauseMessage);
RequestDispatcher rd = httpServletRequest.getRequestDispatcher("/404.jsp"); // 定向的页面
rd.forward(request, response);
} else {
chain.doFilter(request, response);
}
}
public void destroy() {
logger.debug(" destroy {} but actually do nothing.", getClass().getName());
}
}
2、白单验证工具类
因考虑到服务体验,应在不重启服务的情况下实现白单信息的添加,因此采用数据库配置+文件配置结合的形式处理白单信息,为避免拦截器频繁的数据库读取,因此采用数据库白单信息存放于内存中,每隔3个小时时第一个请求则重新读取一次白单信息
package cn.com.zwjp.framework.web.filter.util;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cn.com.zwjp.portal.service.LoginDataService;
import cn.com.zwjp.portal.utils.BaseUtil;
/**
* 白单信息处理
*
* @author 爱兰河少
* @data 2018年8月17日 cn.com.zwjp.framework.filter.service.IgnoreUriUtil
*/
public class IgnoreUriUtil {
private static final Logger log = LoggerFactory.getLogger(IgnoreUriUtil.class);
private static final int IGNORE_URI_TIMEOUT = 60 * 60 * 3;// 超时时间 3小时,此处为秒
/**
* referer跨站点的脚本编制白单
*/
public static Map<List<String>, String> CSRF_IGNORE_URI = new HashMap<List<String>, String>();
/**
* 传入配置白单验证
* @title
* @date 2018年10月9日
* @author 爱兰河少
* @param bdUris
* @param request
* @return
*/
public static boolean isIgnoreUri(List<String> bdUris,ServletRequest request) {
boolean legal = false;
if (bdUris!=null) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String rawReferer = httpServletRequest.getHeader("referer");
if (StringUtils.isNotBlank(rawReferer)) {
// 获取refer的Host信息
String referHost = getHost(rawReferer, false);
if (bdUris.contains(referHost.toLowerCase())) {
legal = true;
}
}
}
return legal;
}
public static boolean isIgnoreUri(ServletRequest request) {
boolean legal = false;
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String rawReferer = httpServletRequest.getHeader("referer");
if (StringUtils.isNotBlank(rawReferer)) {
// 获取refer的Host信息
String referHost = getHost(rawReferer, false);
// 获取服务器的Host信息
String serverHost = request.getServerName();
serverHost = StringUtils.isNotBlank(serverHost) ? serverHost.toLowerCase() : serverHost;
// 获取本地的Host信息,方便开发人员测试
String localIp = httpServletRequest.getLocalAddr();
localIp = StringUtils.isNotBlank(localIp) ? localIp.toLowerCase() : localIp;
if (StringUtils.equals(serverHost, referHost)) {
legal = true;
} else if (StringUtils.equals("localhost", referHost) || StringUtils.equals("127.0.0.1", referHost)
|| StringUtils.equals(localIp, referHost)) {
legal = true;
} else {
legal = isIgnoreUri(referHost);
}
} else {
legal = true;
}
return legal;
}
/**
* 是否在白单,当referHost为空是则认为是在白单信息中
*
* @title
* @date 2018年8月17日
* @author 爱兰河少
* @param referHost
* @return
*/
public static boolean isIgnoreUri(String referHost) {
boolean legal = false;
if (StringUtils.isNotBlank(referHost)) {
boolean newIgnore = false;
List<String> bdUris = null;
if (CSRF_IGNORE_URI.size() > 0) {
String time = null;
for (Map.Entry<List<String>, String> entry : CSRF_IGNORE_URI.entrySet()) {
bdUris = entry.getKey();
time = entry.getValue();
break;
}
// 是否未超时
if (isTimeout(time)) {
newIgnore = true;
bdUris = null;
}
} else {
// 当为空时设置值
newIgnore = true;
}
if (newIgnore) {
bdUris = getBdUris();
// 清空Map
CSRF_IGNORE_URI.clear();
// 设置值
CSRF_IGNORE_URI.put(bdUris, getDate_yyyy_MM_dd_HH_mm_ss());
}
if (bdUris.contains(referHost.toLowerCase())) {
legal = true;
}else{
if(StringUtils.equals("localhost", referHost) || StringUtils.equals("127.0.0.1", referHost)){
legal = true;
}
}
} else {
legal = true;
}
return legal;
}
/**
*
* @title
* @date 2018年8月17日
* @author 爱兰河少
* @param request
* @return
*/
public static boolean isFilterCountSuffix(ServletRequest request) {
boolean legal = false;
// 转换类型
HttpServletRequest req = (HttpServletRequest) request;
String uri = req.getRequestURI();
String suffix = getUrlSuffix(uri);
if ("js".equals(suffix) || "css".equals(suffix) || "jpg".equals(suffix) || "png".equals(suffix)
|| "gif".equals(suffix)) {
legal = true;
}
return legal;
}
/**
* 获取链接的后缀名
*
* @return
*/
private static String getUrlSuffix(String url) {
String suffix = "";
if (StringUtils.isNotBlank(url)) {
int index = url.lastIndexOf(".");
if (index > 0) {
suffix = url.substring(index + 1);
}
String[] spEndUrl = suffix.split("\\?");
suffix = spEndUrl.length > 1 ? spEndUrl[0] : suffix;
spEndUrl = suffix.split("\\,");
suffix = spEndUrl.length > 1 ? spEndUrl[0] : suffix;
spEndUrl = suffix.split("\\;");
suffix = spEndUrl.length > 1 ? spEndUrl[0] : suffix;
suffix = suffix.toLowerCase();
}
return suffix;
}
/**
* 获取白单Host信息
*
* @title
* @date 2018年8月17日
* @author 爱兰河少
* @return
*/
private static List<String> getBdUris() {
List<String> bdUris = new ArrayList<String>();
List<Map<String, Object>> list = getIgnoreUri();
for (Map map : list) {
String v_url = MapUtils.getString(map, "V_URL");
String urlHost = getHost(v_url, true);
if (StringUtils.isNotBlank(urlHost)) {
bdUris.add(urlHost.toLowerCase());
}
}
return bdUris;
}
/**
* 获取url字符串的host
*
* @title
* @date 2018年8月17日
* @author 爱兰河少
* @param v_url
* @param addUrl
* 当url地址不包含http://、https://、ftp://时添加http://前缀
* @return
*/
public static String getHost(String v_url, boolean addUrl) {
String host = null;
try {
if (addUrl && !v_url.toLowerCase().startsWith("http://") && !v_url.toLowerCase().startsWith("https://")
&& !v_url.toLowerCase().startsWith("ftp://")) {
log.info("url读取Host白单信息时,url信息不符合规范:{}", v_url);
v_url = "http://" + v_url;
}
URL url = new URL(v_url);
host = url.getHost();
} catch (MalformedURLException e) {
log.error("Host白单读取出错:{}", e);
}
return host;
}
/**
* 是否超时
*
* @title
* @date 2018年8月17日
* @author 爱兰河少
* @param time
* @return
*/
private static boolean isTimeout(String time) {
String newTime = getDate_yyyy_MM_dd_HH_mm_ss();
long mm = getMiaoCha(newTime, time);
int max = IGNORE_URI_TIMEOUT;
if (mm <= max) {
return false;
}
return true;
}
/**
* 获取当前时间,格式为:年-月-日 时:分:秒
*
* @title
* @date 2018年8月17日
* @author 爱兰河少
* @return
*/
private static String getDate_yyyy_MM_dd_HH_mm_ss() {
return BaseUtil.now_yyyy_MM_dd_HH_mm_ss();
}
/**
* 计算两个时间差,精确到秒
*
* @title
* @date 2018年8月17日
* @author 爱兰河少
* @param newTime
* @param oldTime
* @return
*/
private static long getMiaoCha(String str1, String str2) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
Date one;
Date two;
// long days=0;
long fzs = 0;
try {
one = df.parse(str1);
two = df.parse(str2);
long time1 = one.getTime();
long time2 = two.getTime();
long diff;
if (time1 < time2) {
diff = time2 - time1;
} else {
diff = time1 - time2;
}
// days = diff / (1000 * 60 * 60 * 24);
fzs = diff / 1000;
} catch (ParseException e) {
e.printStackTrace();
}
return fzs;
}
/**
* 获取数据库Url信息, Map对应的数据里面需有Key为V_URL的信息,次参数对应的值则为白单信息
*
* @title
* @date 2018年8月17日
* @author 爱兰河少
* @return
*/
private static List<Map<String, Object>> getIgnoreUri() {
return LoginDataService.getInstance().getIgnoreUri();
}
}
3、web.xml配置
<filter>
<filter-name>CookieFilter</filter-name>
<filter-class>cn.com.zwjp.framework.web.filter.CookieFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CookieFilter</filter-name>
<url-pattern>/*</url-pattern>
<!-- 此处不可加FORWARD,否则Referer信息不在白单时会造成死循环
<dispatcher>REQUEST</dispatcher>
</filter-mapping>