From 7057227573a860b3c82a549481a376ba9104b2bf Mon Sep 17 00:00:00 2001 From: TS-QD1 Date: Wed, 3 Apr 2024 18:32:14 +0800 Subject: [PATCH] =?UTF-8?q?sql=E6=B3=A8=E5=85=A5=E3=80=81xss=E6=B3=A8?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xzszwhy/config/filter/FilterConfig.java | 32 ++ .../filter/sqlinject/SqlInjectFilter.java | 50 ++ .../SqlInjectHttpServletRequestWrapper.java | 81 +++ .../xzszwhy/config/filter/xss/HTMLFilter.java | 522 ++++++++++++++++++ .../xzszwhy/config/filter/xss/XssFilter.java | 64 +++ .../xss/XssHttpServletRequestWrapper.java | 152 +++++ 6 files changed, 901 insertions(+) create mode 100644 src/main/java/cn/com/tenlion/xzszwhy/config/filter/FilterConfig.java create mode 100644 src/main/java/cn/com/tenlion/xzszwhy/config/filter/sqlinject/SqlInjectFilter.java create mode 100644 src/main/java/cn/com/tenlion/xzszwhy/config/filter/sqlinject/SqlInjectHttpServletRequestWrapper.java create mode 100644 src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/HTMLFilter.java create mode 100644 src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/XssFilter.java create mode 100644 src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/XssHttpServletRequestWrapper.java diff --git a/src/main/java/cn/com/tenlion/xzszwhy/config/filter/FilterConfig.java b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/FilterConfig.java new file mode 100644 index 0000000..bece052 --- /dev/null +++ b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/FilterConfig.java @@ -0,0 +1,32 @@ +package cn.com.tenlion.xzszwhy.config.filter; + +import cn.com.tenlion.xzszwhy.config.filter.sqlinject.SqlInjectFilter; +import cn.com.tenlion.xzszwhy.config.filter.xss.XssFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FilterConfig { + + @Bean + public FilterRegistrationBean xssFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new XssFilter()); + registration.addUrlPatterns("/api/*");//过滤所有路径 + registration.setName("xssFilter");//过滤器名称 + registration.setOrder(Integer.MAX_VALUE);//优先级,越低越优先 + return registration; + } + + @Bean + public FilterRegistrationBean sqlInjectFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(new SqlInjectFilter()); + registration.addUrlPatterns("/api/*", "/app/*");//过滤所有路径 + registration.setName("sqlInjectFilter");//过滤器名称 + registration.setOrder(Integer.MAX_VALUE);//优先级,越低越优先 + return registration; + } + +} diff --git a/src/main/java/cn/com/tenlion/xzszwhy/config/filter/sqlinject/SqlInjectFilter.java b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/sqlinject/SqlInjectFilter.java new file mode 100644 index 0000000..f1eb3ba --- /dev/null +++ b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/sqlinject/SqlInjectFilter.java @@ -0,0 +1,50 @@ +package cn.com.tenlion.xzszwhy.config.filter.sqlinject; + +import com.alibaba.excel.util.StringUtils; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class SqlInjectFilter implements Filter { + + private List excludedList = new ArrayList(); + + @Override + public void init(FilterConfig config) throws ServletException { + + /* + * 这里只处理了不拦截的url地址,如果想不拦截某个字段,比如富文本字段, + * 需要自己在XssHttpServletRequestWrapper类中去添加逻辑 + */ + excludedList.add("/test/excluded1"); + excludedList.add("/test/excluded2"); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + SqlInjectHttpServletRequestWrapper sqlInjectHttpServletRequestWrapper = new SqlInjectHttpServletRequestWrapper((HttpServletRequest) request); + String url = sqlInjectHttpServletRequestWrapper.getServletPath(); + if (isExcluded(url)) { + chain.doFilter(request, response); + } else { + // 使用SQL过滤 + chain.doFilter(sqlInjectHttpServletRequestWrapper, response); + } + } + + private boolean isExcluded(String url) { + if (StringUtils.isBlank(url)) { + return false; + } + + for (String excluded : excludedList) { + if (url.contains(excluded)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/cn/com/tenlion/xzszwhy/config/filter/sqlinject/SqlInjectHttpServletRequestWrapper.java b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/sqlinject/SqlInjectHttpServletRequestWrapper.java new file mode 100644 index 0000000..c9d5b29 --- /dev/null +++ b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/sqlinject/SqlInjectHttpServletRequestWrapper.java @@ -0,0 +1,81 @@ +package cn.com.tenlion.xzszwhy.config.filter.sqlinject; + +import com.alibaba.excel.util.StringUtils; +import org.apache.commons.io.IOUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SqlInjectHttpServletRequestWrapper extends HttpServletRequestWrapper { + + private static final Pattern SQL_REGX = Pattern.compile("(select|update|and|or|delete|insert|trancate|char|into|substr|ascii|declare|exec|count|master|drop|execute)", Pattern.CASE_INSENSITIVE); + + + public SqlInjectHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + } + + + @Override + public ServletInputStream getInputStream() throws IOException { + String contentType = super.getHeader(HttpHeaders.CONTENT_TYPE); + //非json类型,直接返回 + if (!(MediaType.APPLICATION_JSON_VALUE. + equalsIgnoreCase(contentType) || + MediaType.APPLICATION_JSON_UTF8_VALUE. + equalsIgnoreCase(contentType))) { + return super.getInputStream(); + } + + //为空,直接返回 + String json = IOUtils.toString(super.getInputStream(), "utf-8"); + if (StringUtils.isBlank(json)) { + return super.getInputStream(); + } + + //xss过滤 + json = clearSqlInject(json); + + final ByteArrayInputStream bis = + new ByteArrayInputStream(json.getBytes("utf-8")); + + return new ServletInputStream() { + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + @Override + public int read() throws IOException { + return bis.read(); + } + }; + } + + private String clearSqlInject(String value) { + String result = value; + Matcher matcher = SQL_REGX.matcher(value); + if (matcher.find()) { + result = result.replaceAll(matcher.group(), ""); + result = clearSqlInject(result); + } + return result; + } +} diff --git a/src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/HTMLFilter.java b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/HTMLFilter.java new file mode 100644 index 0000000..4e925e2 --- /dev/null +++ b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/HTMLFilter.java @@ -0,0 +1,522 @@ +package cn.com.tenlion.xzszwhy.config.filter.xss; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HTMLFilter { + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("\""); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + private static final Pattern P_DOUBLE_QUOT = Pattern.compile("""); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + private boolean vDebug = false; + /** + * flag determining whether to try to make tags when presented with "unbalanced" + * angle brackets (e.g. "" becomes " text "). If set to false, + * unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilter() { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[]{"img"}; + vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"}; + vDisallowed = new String[]{}; + vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp. + vProtocolAtts = new String[]{"src", "href"}; + vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"}; + vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"}; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = true; + } + + /** + * Set debug flag to true. Otherwise use default settings. See the default constructor. + * + * @param debug turn debug on with a true argument + */ + public HTMLFilter(final boolean debug) { + this(); + vDebug = debug; + + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + public HTMLFilter(final Map conf) { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() { + vTagCounts.clear(); + } + + private void debug(final String msg) { + if (vDebug) { + Logger.getAnonymousLogger().info(msg); + } + } + + //--------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + //--------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted + * html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) { + reset(); + String s = input; + + debug("************************************************"); + debug(" INPUT: " + input); + + s = escapeComments(s); + debug(" escapeComments: " + s); + + s = balanceHTML(s); + debug(" balanceHTML: " + s); + + s = checkTags(s); + debug(" checkTags: " + s); + + s = processRemoveBlanks(s); + debug("processRemoveBlanks: " + s); + + s = validateEntities(s); + debug(" validateEntites: " + s); + + debug("************************************************\n\n"); + return s; + } + + public boolean isAlwaysMakeTags() { + return alwaysMakeTags; + } + + public boolean isStripComments() { + return stripComment; + } + + private String escapeComments(final String s) { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) { + final String match = m.group(1); //(.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) { + if (alwaysMakeTags) { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } else { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + s = buf.toString(); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + for (String key : vTagCounts.keySet()) { + for (int ii = 0; ii < vTagCounts.get(key); ii++) { + s += ""; + } + } + + return s; + } + + private String processRemoveBlanks(final String s) { + String result = s; + for (String tag : vRemoveBlanks) { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) { + if (!inArray(name, vSelfClosingTags)) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + //debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) { + String params = ""; + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList(); + final List paramValues = new ArrayList(); + while (m2.find()) { + paramNames.add(m2.group(1)); //([a-z0-9]+) + paramValues.add(m2.group(3)); //(.*?) + } + while (m3.find()) { + paramNames.add(m3.group(1)); //([a-z0-9]+) + paramValues.add(m3.group(3)); //([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + +// debug( "paramName='" + paramName + "'" ); +// debug( "paramValue='" + paramValue + "'" ); +// debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) { + if (inArray(paramName, vProtocolAtts)) { + paramValue = processParamProtocol(paramValue); + } + params += " " + paramName + "=\"" + paramValue + "\""; + } + } + + if (inArray(name, vSelfClosingTags)) { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) { + ending = ""; + } + + if (ending == null || ending.length() < 1) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } else { + vTagCounts.put(name, 1); + } + } else { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } else { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1, s.length()); + if (s.startsWith("#//")) { + s = "#" + s.substring(3, s.length()); + } + } + } + + return s; + } + + private String decodeEntities(String s) { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) { + final String one = m.group(1); //([^&;]*) + final String two = m.group(2); //(?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) { + if (encodeQuotes) { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) { + final String one = m.group(1); //(>|^) + final String two = m.group(2); //([^<]+?) + final String three = m.group(3); //(<|$) + m.appendReplacement(buf, Matcher.quoteReplacement(one + regexReplace(P_QUOTE, """, two) + three)); + } + m.appendTail(buf); + return buf.toString(); + } else { + return s; + } + } + + private String checkEntity(final String preamble, final String term) { + + return ";".equals(term) && isValidEntity(preamble) + ? '&' + preamble + : "&" + preamble; + } + + private boolean isValidEntity(final String entity) { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) { + for (String item : array) { + if (item != null && item.equals(s)) { + return true; + } + } + return false; + } + + private boolean allowed(final String name) { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} diff --git a/src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/XssFilter.java b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/XssFilter.java new file mode 100644 index 0000000..f317c9f --- /dev/null +++ b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/XssFilter.java @@ -0,0 +1,64 @@ +package cn.com.tenlion.xzszwhy.config.filter.xss; + +import com.alibaba.excel.util.StringUtils; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class XssFilter implements Filter { + + //不拦截的地址 + private List excludedList = new ArrayList(); + + @Override + public void init(FilterConfig config) throws ServletException { + + /* + * 这里只处理了不拦截的url地址,如果想不拦截某个字段,比如富文本字段, + * 需要自己在XssHttpServletRequestWrapper类中去添加逻辑 + */ + excludedList.add("/test/excluded1"); + excludedList.add("/test/excluded2"); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); + String url = xssRequest.getServletPath(); + if (isExcluded(url)) { + chain.doFilter(request, response); + } else { + //使用XSS过滤 + chain.doFilter(xssRequest, response); + } + } + + @Override + public void destroy() { + } + + /** + * 是否不拦截 + * + * @param url 请求地址 + * @return true不拦截,false拦截 + */ + private boolean isExcluded(String url) { + if (StringUtils.isBlank(url)) { + return false; + } + + for (String excluded : excludedList) { + if (url.contains(excluded)) { + return true; + } + } + return false; + } + + +} diff --git a/src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/XssHttpServletRequestWrapper.java b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/XssHttpServletRequestWrapper.java new file mode 100644 index 0000000..9807f8c --- /dev/null +++ b/src/main/java/cn/com/tenlion/xzszwhy/config/filter/xss/XssHttpServletRequestWrapper.java @@ -0,0 +1,152 @@ +package cn.com.tenlion.xzszwhy.config.filter.xss; + +import com.alibaba.excel.util.StringUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { + + // 没被包装过的HttpServletRequest(特殊场景,需求自己过滤) + HttpServletRequest orgRequest; + // html过滤 + private final static HTMLFilter htmlFilter = new HTMLFilter(); + + public XssHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + orgRequest = request; + } + + + /** + * 过滤json参数 + */ + @Override + public ServletInputStream getInputStream() throws IOException { + String contentType = super.getHeader(HttpHeaders.CONTENT_TYPE); + //非json类型,直接返回 + if(!(MediaType.APPLICATION_JSON_VALUE. + equalsIgnoreCase(contentType) || + MediaType.APPLICATION_JSON_UTF8_VALUE. + equalsIgnoreCase(contentType))){ + return super.getInputStream(); + } + + //为空,直接返回 + String json = IOUtils.toString(super.getInputStream(), "utf-8"); + if (StringUtils.isBlank(json)) { + return super.getInputStream(); + } + + //xss过滤 + json =xssEncode(json); + json = StringEscapeUtils.unescapeHtml4(json); + + final ByteArrayInputStream bis = + new ByteArrayInputStream(json.getBytes("utf-8")); + + return new ServletInputStream() { + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + @Override + public int read() throws IOException { + return bis.read(); + } + }; + } + + + @Override + public String getParameter(String name) { + String value = super.getParameter(xssEncode(name)); + if (StringUtils.isNotBlank(value)) { + value =xssEncode(value); + } + + return StringEscapeUtils.unescapeHtml4(value); + } + + @Override + public String[] getParameterValues(String name) { + String[] parameters = super.getParameterValues(name); + if (parameters == null || parameters.length == 0) { + return null; + } + + for (int i = 0; i < parameters.length; i++) { + parameters[i] = xssEncode(parameters[i]); + parameters[i] = StringEscapeUtils.unescapeHtml4(parameters[i]); + } + return parameters; + } + + @Override + public Map getParameterMap() { + Map map = new LinkedHashMap<>(); + Map parameters = super.getParameterMap(); + for (String key : parameters.keySet()) { + String[] values = parameters.get(key); + for (int i = 0; i < values.length; i++) { + values[i] = xssEncode(values[i]); + values[i] = StringEscapeUtils.unescapeHtml4(values[i]); + } + map.put(key, values); + } + return map; + } + + @Override + public String getHeader(String name) { + String value = super.getHeader(xssEncode(name)); + if (StringUtils.isNotBlank(value)) { + value = xssEncode(value); + } + + return StringEscapeUtils.unescapeHtml4(value); + } + + private String xssEncode(String input) { + return htmlFilter.filter(input); + } + + /** + * 获取最原始的request + */ + public HttpServletRequest getOrgRequest() { + return orgRequest; + } + + /** + * 获取最原始的request + */ + public static HttpServletRequest getOrgRequest(HttpServletRequest request) { + if (request instanceof XssHttpServletRequestWrapper) { + return ((XssHttpServletRequestWrapper) request).getOrgRequest(); + } + + return request; + } + +}