XSS(Cross Site Scripting), 中文名为跨站脚本, 是发生在目标用户的浏览器层面上的,当渲染DOM树的过程成发生了不在预期内执行的JS代码时,就发生了XSS攻击。跨站脚本的重点不在“跨站”上,而在于“跨站”上。大多数XSS攻击的主要方式是嵌入一段远程或者第三方域上的JS代码。实际上是在目标网站的作用域下执行了这段js代码。
反射型XSS主要表现在浏览器的展示层面上,并没有将脚本数据持久化到数据库中,攻击者通过拦截服务器返回给页面的数据,利用工具在服务器返回的数据中加入攻击脚本,进而在页面上进行展示。严重的还可以窃取用户密码等信息。
顾名思义,它和反射型XSS的差别仅在于,提交的代码会存储在服务器端(数据库,内存,文件系统等),下次请求目标页面时不用再提交XSS代码。最典型的例子是留言板XSS,用户提交一条包含XSS代码的留言存储到数据库,目标用户查看留言板时,那些留言的内容会从数据库查询出来并显示,浏览器发现有XSS代码,就当做正常的HTML与JS解析执行,于是触发了XSS攻击。
如果测试系统是否含有此漏洞,可以使用抓包工具进行测试,在这里我推荐一个工具,也是大多数安全测试人员使用的工具,工具名称为Burpsuite。想要使用此工具还要需要配置一下浏览器。这里主要介绍Burpsuite+Firefox浏览器。
配置方法:http://www.cnblogs.com/slpawn/p/7235105.html
Burpsuite和Firefox证书下载路径:https://download.csdn.net/download/li521wang/10938753
具体的使用方法就不这里详细讲了。
利用Filter进行XSS过滤。首先先增加几个配置类管理XSS过滤参数以及过滤的内容。
/**
* @Author: LX [email protected]
* @Description: 安全过滤配置管理类,由XSSSecurityManger修改。(admin迁移至此)
* @Date: 2019/1/24 14:55
* @Version: V1.0
*/
@Slf4j
public class XSSSecurityManager {
/**
* REGEX:校验正则表达式
*/
public static String REGEX;
public static String[] REGEXS = new String[15];
/**
* 特殊字符匹配
*/
public static Pattern XSS_PATTERN;
/**
* 设置私有构造方法
*/
private XSSSecurityManager() {
}
/**
* @Author: LX [email protected]
* @Description: 初始化过滤匹配的字符
* @Date: 2019/1/24 15:20
* @Version: V1.0
*/
public static void init() {
log.info("XSSSecurityManager.initConfig(String path) begin");
// 匹配含有字符: alert
REGEXS[0] = ".*[A|a][L|l][E|e][R|r][T|t]\\s*\\(.*\\).*";
// 匹配含有字符: window.location =
REGEXS[1] = ".*[W|w][I|i][N|n][D|d][O|o][W|w]\\.[L|l][O|o][C|c][A|a][T|t][I|i][O|o][N|n]\\s*=.*";
// 匹配含有字符:style = x:ex pression ( )
REGEXS[2] = ".*[S|s][T|t][Y|y][L|l][E|e]\\s*=.*[X|x]:[E|e][X|x].*[P|p][R|r][E|e][S|s]{1,2}[I|i][O|o][N|n]\\s*\\(.*\\).*";
// 匹配含有字符: document.cookie
REGEXS[3] = ".*[D|d][O|o][C|c][U|u][M|m][E|e][N|n][T|t]\\.[C|c][O|o]{2}[K|k][I|i][E|e].*";
// 匹配含有字符: eval( )
REGEXS[4] = ".*[E|e][V|v][A|a][L|l]\\s*\\(.*\\).*";
// 匹配含有字符: unescape()
REGEXS[5] = ".*[U|u][N|n][E|e][S|s][C|c][A|a][P|p][E|e]\\s*\\(.*\\).*";
// 匹配含有字符: execscript( )
REGEXS[6] = ".*[E|e][X|x][E|e][C|c][S|s][C|c][R|r][I|i][P|p][T|t]\\s*\\(.*\\).*";
// 匹配含有字符: msgbox( )
REGEXS[7] = ".*[M|m][S|s][G|g][B|b][O|o][X|x]\\s*\\(.*\\).*";
// 匹配含有字符: confirm( )
REGEXS[8] = ".*[C|c][O|o][N|n][F|f][I|i][R|r][M|m]\\s*\\(.*\\).*";
// 匹配含有字符: prompt( )
REGEXS[9] = ".*[P|p][R|r][O|o][M|m][P|p][T|t]\\s*\\(.*\\).*";
// 匹配含有字符:
REGEXS[10] = ".*<[S|s][C|c][R|r][I|i][P|p][T|t]>.*[S|s][C|c][R|r][I|i][P|p][T|t]>.*";
// 匹配含有字符: 含有一个符号: "
REGEXS[11] = "[.&[^\"]]*\"[.&[^\"]]*";
// 匹配含有字符: 含有一个符号: '
REGEXS[12] = "[.&[^']]*'[.&[^']]*";
// 匹配含有字符: 含有回车换行 和
REGEXS[13] = ".&[^a]]|[|a|\n|\r\n|\r|\u0085|\u2028|\u2029]]*<[S|s][C|c][R|r][I|i][P|p][T|t]>.*[S|s][C|c][R|r][I|i][P|p][T|t]>[[.&[^a]]|[|a|\n|\r\n|\r|\u0085|\u2028|\u2029]]*";
// 匹配特殊sql字符
REGEXS[14] = "(?:')|(?:--)|(/\\*(?:.|[\\n\\r])*?\\*/)|" + "(.*\\b(select|update|and|or|delete|insert|trancate|char|into|substr|ascii|declare|exec|count|master|into|drop|execute)\\b.*)";
StringBuffer sb = new StringBuffer("^");
for (String tmp : REGEXS) {
tmp = tmp.replaceAll("\\\\\\\\", "\\\\");
sb.append(tmp);
sb.append("|");
}
if (sb.charAt(sb.length() - 1) == '|') {
REGEX = sb.substring(0, sb.length() - 1) + "$";
log.info("安全匹配规则" + REGEX);
} else {
log.error("安全过滤配置文件加载失败:正则表达式异常 " + sb.toString());
}
// 生成匹配器
XSS_PATTERN = Pattern.compile(REGEX);
log.info("XSSSecurityManager.initConfig(String path) end");
}
/**
* 匹配字符是否含特殊字符
*
* @param text
* @return
*/
public static boolean matches(String text) {
if (StringUtils.isBlank(text)) {
return false;
}
return XSS_PATTERN.matcher(text).matches();
}
}
/**
* @Author: LX [email protected]
* @Description: XSS配置类
* @Date: 2019/1/24 14:54
* @Version: V1.0
*/
public class XSSSecurityConfig {
private XSSSecurityConfig() {
}
/**
* CHECK_HEADER:是否开启header校验
*/
public static boolean IS_CHECK_HEADER = false;
/**
* CHECK_PARAMETER:是否开启parameter校验
*/
public static boolean IS_CHECK_PARAMETER = true;
/**
* IS_LOG:是否记录日志
*/
public static boolean IS_LOG = true;
/**
* IS_LOG:是否中断操作
*/
public static boolean IS_CHAIN = false;
/**
* REPLACE:是否开启替换
*/
public static boolean REPLACE = true;
/**
* FILTER_ERROR_PAGE:过滤后错误页面
*/
public static String FILTER_ERROR_PAGE2 = "/error";
/**
* IS_FILTER_REFERER:是否开启防盗链
*/
public static boolean IS_FILTER_REFERER = false;
}
/**
* @Author: LX [email protected]
* @Description: 自定义XSS过滤wrapper
* @Date: 2019/1/24 14:50
* @Version: V1.0
*/
@Slf4j
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
// // 若开启特殊字符替换,对特殊字符进行替换
// if (XSSSecurityConfig.REPLACE && StringUtils.isNotBlank(value)) {
// return stringFilter(value);
// }
return value;
}
@Override
public String getQueryString() {
String queryString = super.getQueryString();
// 若开启特殊字符替换,对特殊字符进行替换
if (XSSSecurityConfig.REPLACE && StringUtils.isNotBlank(queryString)) {
return stringFilter(queryString);
}
return StringEscapeUtils.escapeHtml4(super.getQueryString());
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
// 若开启特殊字符替换,对特殊字符进行替换
if (XSSSecurityConfig.REPLACE && StringUtils.isNotBlank(value)) {
return stringFilter(value);
}
return value;
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values != null) {
int length = values.length;
String[] escapseValues = new String[length];
for (int i = 0; i < length; i++) {
escapseValues[i] = StringEscapeUtils.escapeHtml4(values[i]);
}
return escapseValues;
}
return super.getParameterValues(name);
}
/**
* 没有违规的数据,就返回false;
* 若存在违规数据,根据配置信息判断是否跳转到错误页面
*
* @return
* @throws IOException
* @throws ServletException
*/
public boolean validateParameter() {
// 开始header校验,对header信息进行校验
if (XSSSecurityConfig.IS_CHECK_HEADER) {
if (this.checkHeader()) {
return true;
}
}
// 开始parameter校验,对parameter信息进行校验
if (XSSSecurityConfig.IS_CHECK_PARAMETER) {
if (this.checkParameter()) {
return true;
}
}
return false;
}
/**
* 没有违规的数据,就返回false;
*
* @return
*/
private boolean checkHeader() {
Enumeration<String> headerParams = this.getHeaderNames();
while (headerParams.hasMoreElements()) {
String headerName = headerParams.nextElement();
String headerValue = this.getHeader(headerName);
if (XSSSecurityManager.matches(headerValue)) {
return true;
}
}
return false;
}
/**
* 没有违规的数据,就返回false;
*
* @return
*/
private boolean checkParameter() {
Map<String, String[]> submitParams = this.getParameterMap();
Set<String> submitNames = submitParams.keySet();
for (String submitName : submitNames) {
String[] submitValues = submitParams.get(submitName);
for (String submitValue : submitValues) {
try {
submitValue = StringEscapeUtils.unescapeHtml4(submitValue);
log.debug(submitName + ":" + submitValue + "----" + XSSSecurityManager.matches(submitValue));
if (XSSSecurityManager.matches(submitValue)) {
return true;
}
} catch (Exception e) {
log.info("地址解码异常:" + submitValue);
return false;
}
}
}
return false;
}
/**
* 过滤字符串里的的特殊字符
*
* @param str 要过滤的字符串
* @return 过滤后的字符串
*/
public static String stringFilter(String str) {
String temp = StringUtils.replace(str, "%27", "");
temp = StringUtils.replace(temp, "*", "");
temp = StringUtils.replace(temp, "\"", """);
temp = StringUtils.replace(temp, "'", "");
temp = StringUtils.replace(temp, "\\\"", "");
temp = StringUtils.replace(temp, ";", "");
temp = StringUtils.replace(temp, "<", "<");
temp = StringUtils.replace(temp, ">", ">");
temp = StringUtils.replace(temp, "(", "");
temp = StringUtils.replace(temp, ")", "");
temp = StringUtils.replace(temp, "{", "");
temp = StringUtils.replace(temp, "}", "");
return temp.trim();
}
}
/**
* @Author: LX [email protected]
* @Description: XSS过滤器 拦截所有请求,对Header、Parameter进行过滤
* @Date: 2019/1/24 14:51
* @Version: V1.0
*/
@Slf4j
public class XssFilter implements Filter {
/**
* @Author: LX [email protected]
* @Description: 项目启动时初始化
* @Date: 2019/1/24 16:11
* @Version: V1.0
*/
@Override
public void init(FilterConfig filterConfig) {
//初始化XSS过滤词库
XSSSecurityManager.init();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// LX [email protected] 此处增加CSRF过滤 下编讲
// http信息封装类
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(req);
String pageLink = xssRequest.getServletPath();
if (StringUtils.isNotBlank(pageLink)) {
// 对request信息进行封装并进行校验工作,若校验失败(含非法字符),
// 根据配置信息进行日志记录和请求中断处理
if (!pageLink.equals(XSSSecurityConfig.FILTER_ERROR_PAGE2)
&& xssRequest.validateParameter()) {
//判断是否记录日志
if (XSSSecurityConfig.IS_LOG) {
// 记录攻击访问日志 可使用数据库、日志、文件等方式
log.error("访问IP:{},请求后缀:{},查询参数:{}", request.getRemoteAddr(),
pageLink, xssRequest.getQueryString());
}
if (XSSSecurityConfig.IS_CHAIN) {
//进行页面跳转
request.getRequestDispatcher(XSSSecurityConfig.FILTER_ERROR_PAGE2).forward(request, resp);
return;
}
chain.doFilter(xssRequest, resp);
} else {
chain.doFilter(xssRequest, resp);
}
} else {
chain.doFilter(xssRequest, resp);
}
}
/**
* @Author: LX [email protected]
* @Description: 生命周期结束时调用
* @Date: 2019/1/24 16:11
* @Version: V1.0
*/
@Override
public void destroy() {
}
}
<filter>
<filter-name>XssFilterfilter-name>
<filter-class>com.***.XssFilterfilter-class>
filter>
<filter-mapping>
<filter-name>XssFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
到此处将修改后的代码部署到服务器。利用Burpsuite再次进行安全测试,XSS攻击已经被我们过滤了。如果大家还有其他比较好的XSS防护对策,可以分享一下。万分感谢!