如果一個網站是從其他網站點擊後跳轉過來,則會在當前的請求頭中帶上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>