跨站脚本攻击过滤器

This commit is contained in:
gucl
2016-03-18 15:16:39 +08:00
commit 7b04ec2cda
7 changed files with 784 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
.gradle
build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
.classpath
.project
.settings/
/bin/

122
build.gradle Normal file
View File

@@ -0,0 +1,122 @@
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'maven-publish'
group="com.ai.opt.xss.filter"
ext{
builtBy="gradle 2.2"
publishUserName = "runnerdev"
publishUserPassword = "runnerdev"
// publishURL="http://223.202.119.155:18081/nexus/content/repositories/thirdparty/"
//测试分支与生产分支发布URL
publishURL = "http://10.1.228.199:18081/nexus/content/repositories/snapshots/"
//jar包的版本信息
appVersion="1.0-SNAPSHOT"
appName="opt-xss-firewall"
}
sourceCompatibility = 1.7
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
sourceSets {
main {
java { srcDirs = ['src/main/java']}
resources {
srcDirs = ['src/main/resources']
}
}
}
repositories {
maven{ url "http://10.1.228.199:18081/nexus/content/groups/public/"}
}
jar{
baseName appName
version appVersion
manifest {
attributes 'packageName': appName, 'Built-By': builtBy,'create-date': new Date().format('yyyy-MM-dd HH:mm:ss')
}
}
artifacts {archives jar}
publishing {
publications {
publishing.publications.create("appJarPublish", MavenPublication) {
groupId group
artifactId appName
version appVersion
artifact jar
pom.withXml {
asNode().children().last() + {
delegate.dependencies {
delegate.dependency {
delegate.groupId("org.slf4j")
delegate.artifactId("slf4j-api")
delegate.version("1.7.12")
}
delegate.dependency {
delegate.groupId("commons-logging")
delegate.artifactId("commons-logging")
delegate.version("1.2")
}
delegate.dependency {
delegate.groupId("commons-lang")
delegate.artifactId("commons-lang")
delegate.version("2.6")
}
delegate.dependency {
delegate.groupId("javax.servlet.jsp")
delegate.artifactId("javax.servlet.jsp-api")
delegate.version("2.3.1")
}
delegate.dependency {
delegate.groupId("javax.servlet")
delegate.artifactId("javax.servlet-api")
delegate.version("3.1.0")
}
delegate.dependency {
delegate.groupId("org.owasp.antisamy")
delegate.artifactId("antisamy")
delegate.version("1.5.3")
}
delegate.dependency {
delegate.groupId("com.alibaba")
delegate.artifactId("fastjson")
delegate.version("1.2.6")
}
}
}
}
}
}
repositories {
maven {
url publishURL
credentials {
username = publishUserName
password = publishUserPassword
}
}
}
}
dependencies {
compile 'org.slf4j:slf4j-api:1.7.12'
compile 'commons-logging:commons-logging:1.2'
compile 'commons-lang:commons-lang:2.6'
compile "javax.servlet:javax.servlet-api:3.1.0"
compile "javax.servlet.jsp:javax.servlet.jsp-api:2.3.1"
compile 'org.owasp.antisamy:antisamy:1.5.3'
compile 'com.alibaba:fastjson:1.2.6'
}

View File

@@ -0,0 +1,105 @@
package com.ai.net.xss.filter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ai.net.xss.util.CollectionUtil;
import com.ai.net.xss.util.StringUtil;
import com.ai.net.xss.wrapper.XssRequestWrapper;
import com.alibaba.fastjson.JSON;
public class XSSFilter implements Filter {
private static Logger log=LoggerFactory.getLogger(XSSFilter.class);
private static final String IGNORE_PATH="ignorePath"; //可放行的请求路径
private static final String IGNORE_PARAM_VALUE="ignoreParamValue";//可放行的参数值
private List<String> ignorePathList;//可放行的请求路径列表
private List<String> ignoreParamValueList;//可放行的参数值列表
//默认放行单点登录的登出响应(响应中包含samlp:LogoutRequest标签直接放行)
private static final String CAS_LOGOUT_RESPONSE_TAG="samlp:LogoutRequest";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("XSS fiter [XSSFilter] init start ...");
String ignorePaths = filterConfig.getInitParameter(IGNORE_PATH);
String ignoreParamValues = filterConfig.getInitParameter(IGNORE_PARAM_VALUE);
if (!StringUtil.isBlank(ignorePaths)) {
String[] ignorePathArr = ignorePaths.split(",");
ignorePathList=Arrays.asList(ignorePathArr);
}
if (!StringUtil.isBlank(ignoreParamValues)) {
String[] ignoreParamValueArr = ignoreParamValues.split(",");
ignoreParamValueList=Arrays.asList(ignoreParamValueArr);
//默认放行单点登录的登出响应(响应中包含samlp:LogoutRequest标签直接放行)
if(!ignoreParamValueList.contains(CAS_LOGOUT_RESPONSE_TAG)){
ignoreParamValueList.add(CAS_LOGOUT_RESPONSE_TAG);
}
}
else{
//默认放行单点登录的登出响应(响应中包含samlp:LogoutRequest标签直接放行)
ignoreParamValueList=new ArrayList<String>();
ignoreParamValueList.add(CAS_LOGOUT_RESPONSE_TAG);
}
log.info("ignorePathList="+JSON.toJSONString(ignorePathList));
log.info("ignoreParamValueList="+JSON.toJSONString(ignoreParamValueList));
log.info("XSS fiter [XSSFilter] init end");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("XSS fiter [XSSFilter] starting");
// 判断uri是否包含项目名称
String uriPath = ((HttpServletRequest) request).getRequestURI();
if (isIgnorePath(uriPath)) {
log.info("ignore xssfilter,path["+uriPath+"] no need XssFilter, go ahead...");
chain.doFilter(request, response);
return;
} else {
log.info("has xssfiter path["+uriPath+"] need XssFilter, go to XssRequestWrapper");
chain.doFilter(new XssRequestWrapper((HttpServletRequest) request,ignoreParamValueList), response);
}
log.info("XSS fiter [XSSFilter] stop");
}
@Override
public void destroy() {
log.info("XSS fiter [XSSFilter] destroy");
}
private boolean isIgnorePath(String servletPath) {
if(StringUtil.isBlank(servletPath)){
return true;
}
if (CollectionUtil.isEmpty(ignorePathList))
{
return false;
}
else{
for(String ignorePath:ignorePathList){
if(servletPath.contains(ignorePath)){
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,57 @@
package com.ai.net.xss.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public final class CollectionUtil {
private CollectionUtil(){}
public static boolean isEmpty(Collection<?> collection) {
if (null == collection) {
return true;
} else {
return collection.isEmpty();
}
}
public static boolean isEmpty(Object[] objects) {
return (objects == null || objects.length == 0) ? true : false;
}
/**
* 数组转换为List
*
* @param arr
* @return
*/
public static List<?> arrayToList(Object[] arr) {
List<?> list = new ArrayList<>();
if (arr == null)
{return list;}
list = Arrays.asList(arr);
return list;
}
/**
* 集合分割成字符串
* @param collections 集合对象
* @param separator 分隔符
* @return
* @author rui
*/
public static String split(Collection<?> collections, String separator){
Object[] array = collections.toArray(new Object[0]);
int length = array.length;
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < length; i++) {
stringBuilder.append(array[i]);
if (i != length-1) {
stringBuilder.append(separator);
}
}
return stringBuilder.toString();
}
}

View File

@@ -0,0 +1,164 @@
package com.ai.net.xss.util;
import java.security.SecureRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class StringUtil {
private StringUtil() {
}
public static boolean isBlank(String str) {
if (null == str) {
return true;
}
if ("".equals(str.trim())) {
return true;
}
return false;
}
public static String toString(Object obj) {
if (obj == null) {
return "";
}
return obj.toString();
}
public static String restrictLength(String strSrc, int iMaxLength) {
if (strSrc == null) {
return null;
}
if (iMaxLength <= 0) {
return strSrc;
}
String strResult = strSrc;
byte[] b = null;
int iLength = strSrc.length();
if (iLength > iMaxLength) {
strResult = strResult.substring(0, iMaxLength);
iLength = iMaxLength;
}
while (true) {
b = strResult.getBytes();
if (b.length <= iMaxLength) {
break;
}
iLength--;
strResult = strResult.substring(0, iLength);
}
return strResult;
}
public static String getRandomString(int length) {
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
// Random random = new Random();
SecureRandom random = new SecureRandom();
StringBuilder buf = new StringBuilder();
for (int i = 0; i < length; i++) {
int num = random.nextInt(str.length());
buf.append(str.charAt(num));
}
return buf.toString();
}
/**
* 左补齐
*
* @param target
* 目标字符串
* @param fix
* 补齐字符
* @param length
* 目标长度
* @return
*/
public static String lPad(String target, String fix, int length) {
if (target == null || fix == null || !(target.length() < length)) {
return target;
}
StringBuilder newStr = new StringBuilder();
for (int i = 0; i < length - target.length(); i++) {
newStr.append(fix);
}
return newStr.append(target).toString();
}
/**
* 右补齐
*
* @param target
* 目标字符串
* @param fix
* 补齐字符
* @param length
* 目标长度
* @return
*/
public static String rPad(String target, String fix, int length) {
if (target == null || fix == null || !(target.length() < length)) {
return target;
}
StringBuilder newStr = new StringBuilder();
newStr.append(target);
for (int i = 0; i < length - target.length(); i++) {
newStr.append(fix);
}
return newStr.toString();
}
/**
* 字符串数据join操作
*
* @param strs
* @param spi
* @return
* @author zhoubo
*/
public static String join(String[] strs, String spi) {
StringBuilder buf = new StringBuilder();
int step = 0;
for (String str : strs) {
buf.append(str);
if (step++ < strs.length - 1) {
buf.append(spi);
}
}
return buf.toString();
}
// 默认值为无
public static String toString2(Object obj) {
if (obj == null) {
return "";
} else if ("".equals(obj)) {
return "";
}
return obj.toString();
}
/*
* public static void main(String[] args){ System.out.println(StringUtil.getRandomString(10)); }
*/
/**
* 固网号码去除 区号-号码 中间的横杠 010-88018802
*
* @param str
* @return
* @author mayt
*/
public static String replaceServiceNumBar(String str) {
String dest = "";
if (str != null) {
Pattern p = Pattern.compile("-");
Matcher m = p.matcher(str);
dest = m.replaceAll("");
}
return dest;
}
}

View File

@@ -0,0 +1,142 @@
package com.ai.net.xss.wrapper;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.apache.commons.lang.StringEscapeUtils;
import org.owasp.validator.html.AntiSamy;
import org.owasp.validator.html.CleanResults;
import org.owasp.validator.html.Policy;
import org.owasp.validator.html.PolicyException;
import org.owasp.validator.html.ScanException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ai.net.xss.util.CollectionUtil;
import com.ai.net.xss.util.StringUtil;
import com.alibaba.fastjson.JSON;
public class XssRequestWrapper extends HttpServletRequestWrapper {
private static Logger log=LoggerFactory.getLogger(XssRequestWrapper.class);
private List<String> ignoreParamValueList;
private static final String ANTISAMY_SLASHDOT_XML = "antisamy-slashdot-1.4.4.xml";
private static Policy policy = null;
static {
log.info(" start read XSS configfile [" + ANTISAMY_SLASHDOT_XML + "]");
InputStream inputStream = XssRequestWrapper.class.getClassLoader().getResourceAsStream(ANTISAMY_SLASHDOT_XML);
try {
policy = Policy.getInstance(inputStream);
log.info("read XSS configfile [" + ANTISAMY_SLASHDOT_XML + "] success");
} catch (PolicyException e) {
log.error("read XSS configfile [" + ANTISAMY_SLASHDOT_XML + "] fail , reason:", e);
}
finally{
if(inputStream!=null){
try {
inputStream.close();
} catch (IOException e) {
log.error("close XSS configfile [" + ANTISAMY_SLASHDOT_XML + "] fail , reason:", e);
}
}
}
}
public XssRequestWrapper(HttpServletRequest request,List<String> ignoreParamValueList) {
super(request);
this.ignoreParamValueList=ignoreParamValueList;
}
@SuppressWarnings("rawtypes")
public Map<String, String[]> getParameterMap() {
Map<String, String[]> request_map = super.getParameterMap();
Iterator iterator = request_map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry me = (Map.Entry) iterator.next();
log.info(me.getKey()+":");
String[] values = (String[]) me.getValue();
for (int i = 0; i < values.length; i++) {
log.info(values[i]);
values[i] = xssClean(values[i]);
}
}
return request_map;
}
public String[] getParameterValues(String paramString) {
String[] arrayOfString1 = super.getParameterValues(paramString);
if (arrayOfString1 == null)
return null;
int i = arrayOfString1.length;
String[] arrayOfString2 = new String[i];
for (int j = 0; j < i; j++)
arrayOfString2[j] = xssClean(arrayOfString1[j]);
return arrayOfString2;
}
public String getParameter(String paramString) {
String str = super.getParameter(paramString);
if (str == null)
return null;
return xssClean(str);
}
public String getHeader(String paramString) {
String str = super.getHeader(paramString);
if (str == null)
return null;
return xssClean(str);
}
private String xssClean(String paramValue) {
AntiSamy antiSamy = new AntiSamy();
log.info("ignoreParamValueList="+JSON.toJSONString(ignoreParamValueList));
try {
log.info("raw value before xssClean: " + paramValue);
if(isIgnoreParamValue(paramValue)){
log.info("ignore the xssClean,keep the raw paramValue: " + paramValue);
return paramValue;
}
else{
final CleanResults cr = antiSamy.scan(paramValue, policy);
String str = StringEscapeUtils.escapeHtml(cr.getCleanHTML());
str = str.replaceAll((antiSamy.scan("&nbsp;", policy)).getCleanHTML(), "");
str = StringEscapeUtils.unescapeHtml(str);
log.info("xssfilter value after xssClean" + str);
return str;
}
} catch (ScanException e) {
log.error("scan failed parmter is [" + paramValue + "]", e);
} catch (PolicyException e) {
log.error("antisamy convert failed parmter is [" + paramValue + "]", e);
}
return paramValue;
}
private boolean isIgnoreParamValue(String paramValue) {
if(StringUtil.isBlank(paramValue)){
return true;
}
if (CollectionUtil.isEmpty(ignoreParamValueList))
{
return false;
}
else {
for(String ignoreParamValue:ignoreParamValueList){
if(paramValue.contains(ignoreParamValue)){
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
W3C rules retrieved from:
http://www.w3.org/TR/html401/struct/global.html
-->
<!--
Slashdot allowed tags taken from "Reply" page:
<b> <i> <p> <br> <a> <ol> <ul> <li> <dl> <dt> <dd> <em> <strong> <tt> <blockquote> <div> <ecode> <quote>
-->
<anti-samy-rules xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="antisamy.xsd">
<directives>
<directive name="omitXmlDeclaration" value="true"/>
<directive name="omitDoctypeDeclaration" value="true"/>
<directive name="maxInputSize" value="5000"/>
<directive name="useXHTML" value="true"/>
<directive name="formatOutput" value="true"/>
<directive name="embedStyleSheets" value="false"/>
</directives>
<common-regexps>
<!--
From W3C:
This attribute assigns a class name or set of class names to an
element. Any number of elements may be assigned the same class
name or names. Multiple class names must be separated by white
space characters.
-->
<regexp name="htmlTitle" value="[\p{L}\p{N}\s\-_',:\[\]!\./\\\(\)&amp;]*"/> <!-- force non-empty with a '+' at the end instead of '*' -->
<regexp name="onsiteURL" value="([\p{L}\p{N}\\/\.\?=\#&amp;;\-_~]+|\#(\w)+)"/>
<regexp name="offsiteURL" value="(\s)*((ht|f)tp(s?)://|mailto:)[\p{L}\p{N}]+[~\p{L}\p{N}\p{Zs}\-_\.@\#\$%&amp;;:,\?=/\+!\(\)]*(\s)*"/>
</common-regexps>
<!--
Tag.name = a, b, div, body, etc.
Tag.action = filter: remove tags, but keep content, validate: keep content as long as it passes rules, remove: remove tag and contents
Attribute.name = id, class, href, align, width, etc.
Attribute.onInvalid = what to do when the attribute is invalid, e.g., remove the tag (removeTag), remove the attribute (removeAttribute), filter the tag (filterTag)
Attribute.description = What rules in English you want to tell the users they can have for this attribute. Include helpful things so they'll be able to tune their HTML
-->
<!--
Some attributes are common to all (or most) HTML tags. There aren't many that qualify for this. You have to make sure there's no
collisions between any of these attribute names with attribute names of other tags that are for different purposes.
-->
<common-attributes>
<attribute name="lang" description="The 'lang' attribute tells the browser what language the element's attribute values and content are written in">
<regexp-list>
<regexp value="[a-zA-Z]{2,20}"/>
</regexp-list>
</attribute>
<attribute name="title" description="The 'title' attribute provides text that shows up in a 'tooltip' when a user hovers their mouse over the element">
<regexp-list>
<regexp name="htmlTitle"/>
</regexp-list>
</attribute>
<attribute name="href" onInvalid="filterTag">
<regexp-list>
<regexp name="onsiteURL"/>
<regexp name="offsiteURL"/>
</regexp-list>
</attribute>
<attribute name="align" description="The 'align' attribute of an HTML element is a direction word, like 'left', 'right' or 'center'">
<literal-list>
<literal value="center"/>
<literal value="left"/>
<literal value="right"/>
<literal value="justify"/>
<literal value="char"/>
</literal-list>
</attribute>
</common-attributes>
<!--
This requires normal updates as browsers continue to diverge from the W3C and each other. As long as the browser wars continue
this is going to continue. I'm not sure war is the right word for what's going on. Doesn't somebody have to win a war after
a while?
-->
<global-tag-attributes>
<attribute name="title"/>
<attribute name="lang"/>
</global-tag-attributes>
<tags-to-encode>
<tag>g</tag>
<tag>grin</tag>
</tags-to-encode>
<tag-rules>
<!-- Tags related to JavaScript -->
<tag name="script" action="remove"/>
<tag name="noscript" action="remove"/>
<!-- Frame & related tags -->
<tag name="iframe" action="remove"/>
<tag name="frameset" action="remove"/>
<tag name="frame" action="remove"/>
<tag name="noframes" action="remove"/>
<!-- CSS related tags -->
<tag name="style" action="remove"/>
<!-- All reasonable formatting tags -->
<tag name="p" action="validate">
<attribute name="align"/>
</tag>
<tag name="div" action="validate"/>
<tag name="i" action="validate"/>
<tag name="b" action="validate"/>
<tag name="em" action="validate"/>
<tag name="blockquote" action="validate"/>
<tag name="tt" action="validate"/>
<tag name="strong" action="validate"/>
<tag name="br" action="truncate"/>
<!-- Custom Slashdot tags, though we're trimming the idea of having a possible mismatching end tag with the endtag="" attribute -->
<tag name="quote" action="validate"/>
<tag name="ecode" action="validate"/>
<!-- Anchor and anchor related tags -->
<tag name="a" action="validate">
<attribute name="href" onInvalid="filterTag"/>
<attribute name="nohref">
<literal-list>
<literal value="nohref"/>
<literal value=""/>
</literal-list>
</attribute>
<attribute name="rel">
<literal-list>
<literal value="nofollow"/>
</literal-list>
</attribute>
</tag>
<!-- List tags -->
<tag name="ul" action="validate"/>
<tag name="ol" action="validate"/>
<tag name="li" action="validate"/>
</tag-rules>
<!-- No CSS on Slashdot posts -->
<css-rules>
</css-rules>
</anti-samy-rules>