潘志宝
2024-12-15 c50decb8e57c032f7bb8c52565ce8b8dece27441
提交 | 用户 | 时间
a6de49 1 package com.iailab.module.data.common.xss;
H 2
3 import java.util.*;
4 import java.util.concurrent.ConcurrentHashMap;
5 import java.util.concurrent.ConcurrentMap;
6 import java.util.logging.Logger;
7 import java.util.regex.Matcher;
8 import java.util.regex.Pattern;
9
10 /**
11  *
12  * HTML filtering utility for protecting against XSS (Cross Site Scripting).
13  *
14  * This code is licensed LGPLv3
15  *
16  * This code is a Java port of the original work in PHP by Cal Hendersen.
17  * http://code.iamcal.com/php/lib_filter/
18  *
19  * The trickiest part of the translation was handling the differences in regex handling
20  * between PHP and Java.  These resources were helpful in the process:
21  *
22  * http://java.sun.com/j2se/1.4.2/docs/api/java/util/regex/Pattern.html
23  * http://us2.php.net/manual/en/reference.pcre.pattern.modifiers.php
24  * http://www.regular-expressions.info/modifiers.html
25  *
26  * A note on naming conventions: instance variables are prefixed with a "v"; global
27  * constants are in all caps.
28  *
29  * Sample use:
30  * String input = ...
31  * String clean = new HTMLFilter().filter( input );
32  *
33  * The class is not thread safe. Create a new instance if in doubt.
34  *
35  * If you find bugs or have suggestions on improvement (especially regarding
36  * performance), please contact us.  The latest version of this
37  * source, and our contact details, can be found at http://xss-html-filter.sf.net
38  *
39  * @author Joseph O'Connell
40  * @author Cal Hendersen
41  * @author Michael Semb Wever
42  */
43 public final class HTMLFilter {
44
45     /** regex flag union representing /si modifiers in php **/
46     private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
47     private static final Pattern P_COMMENTS = Pattern.compile("<!--(.*?)-->", Pattern.DOTALL);
48     private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI);
49     private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL);
50     private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI);
51     private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI);
52     private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI);
53     private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI);
54     private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI);
55     private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?");
56     private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?");
57     private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?");
58     private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))");
59     private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL);
60     private static final Pattern P_END_ARROW = Pattern.compile("^>");
61     private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)");
62     private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)");
63     private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)");
64     private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)");
65     private static final Pattern P_AMP = Pattern.compile("&");
66     private static final Pattern P_QUOTE = Pattern.compile("<");
67     private static final Pattern P_LEFT_ARROW = Pattern.compile("<");
68     private static final Pattern P_RIGHT_ARROW = Pattern.compile(">");
69     private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>");
70
71     // @xxx could grow large... maybe use sesat's ReferenceMap
72     private static final ConcurrentMap<String,Pattern> P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<String, Pattern>();
73     private static final ConcurrentMap<String,Pattern> P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<String, Pattern>();
74
75     /** set of allowed html elements, along with allowed attributes for each element **/
76     private final Map<String, List<String>> vAllowed;
77     /** counts of open tags for each (allowable) html element **/
78     private final Map<String, Integer> vTagCounts = new HashMap<String, Integer>();
79
80     /** html elements which must always be self-closing (e.g. "<img />") **/
81     private final String[] vSelfClosingTags;
82     /** html elements which must always have separate opening and closing tags (e.g. "<b></b>") **/
83     private final String[] vNeedClosingTags;
84     /** set of disallowed html elements **/
85     private final String[] vDisallowed;
86     /** attributes which should be checked for valid protocols **/
87     private final String[] vProtocolAtts;
88     /** allowed protocols **/
89     private final String[] vAllowedProtocols;
90     /** tags which should be removed if they contain no content (e.g. "<b></b>" or "<b />") **/
91     private final String[] vRemoveBlanks;
92     /** entities allowed within html markup **/
93     private final String[] vAllowedEntities;
94     /** flag determining whether comments are allowed in input String. */
95     private final boolean stripComment;
96     private final boolean encodeQuotes;
97     private boolean vDebug = false;
98     /**
99      * flag determining whether to try to make tags when presented with "unbalanced"
100      * angle brackets (e.g. "<b text </b>" becomes "<b> text </b>").  If set to false,
101      * unbalanced angle brackets will be html escaped.
102      */
103     private final boolean alwaysMakeTags;
104
105     /** Default constructor.
106      *
107      */
108     public HTMLFilter() {
109         vAllowed = new HashMap<>();
110
111         final ArrayList<String> a_atts = new ArrayList<String>();
112         a_atts.add("href");
113         a_atts.add("target");
114         vAllowed.put("a", a_atts);
115
116         final ArrayList<String> img_atts = new ArrayList<String>();
117         img_atts.add("src");
118         img_atts.add("width");
119         img_atts.add("height");
120         img_atts.add("alt");
121         vAllowed.put("img", img_atts);
122
123         final ArrayList<String> no_atts = new ArrayList<String>();
124         vAllowed.put("b", no_atts);
125         vAllowed.put("strong", no_atts);
126         vAllowed.put("i", no_atts);
127         vAllowed.put("em", no_atts);
128
129         vSelfClosingTags = new String[]{"img"};
130         vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"};
131         vDisallowed = new String[]{};
132         vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp.
133         vProtocolAtts = new String[]{"src", "href"};
134         vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"};
135         vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"};
136         stripComment = true;
137         encodeQuotes = true;
138         alwaysMakeTags = true;
139     }
140
141     /** Set debug flag to true. Otherwise use default settings. See the default constructor.
142      *
143      * @param debug turn debug on with a true argument
144      */
145     public HTMLFilter(final boolean debug) {
146         this();
147         vDebug = debug;
148
149     }
150
151     /** Map-parameter configurable constructor.
152      *
153      * @param conf map containing configuration. keys match field names.
154      */
155     public HTMLFilter(final Map<String,Object> conf) {
156
157         assert conf.containsKey("vAllowed") : "configuration requires vAllowed";
158         assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags";
159         assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags";
160         assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed";
161         assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols";
162         assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts";
163         assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks";
164         assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities";
165
166         vAllowed = Collections.unmodifiableMap((HashMap<String, List<String>>) conf.get("vAllowed"));
167         vSelfClosingTags = (String[]) conf.get("vSelfClosingTags");
168         vNeedClosingTags = (String[]) conf.get("vNeedClosingTags");
169         vDisallowed = (String[]) conf.get("vDisallowed");
170         vAllowedProtocols = (String[]) conf.get("vAllowedProtocols");
171         vProtocolAtts = (String[]) conf.get("vProtocolAtts");
172         vRemoveBlanks = (String[]) conf.get("vRemoveBlanks");
173         vAllowedEntities = (String[]) conf.get("vAllowedEntities");
174         stripComment =  conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true;
175         encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true;
176         alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true;
177     }
178
179     private void reset() {
180         vTagCounts.clear();
181     }
182
183     private void debug(final String msg) {
184         if (vDebug) {
185             Logger.getAnonymousLogger().info(msg);
186         }
187     }
188
189     //---------------------------------------------------------------
190     // my versions of some PHP library functions
191     public static String chr(final int decimal) {
192         return String.valueOf((char) decimal);
193     }
194
195     public static String htmlSpecialChars(final String s) {
196         String result = s;
197         result = regexReplace(P_AMP, "&amp;", result);
198         result = regexReplace(P_QUOTE, "&quot;", result);
199         result = regexReplace(P_LEFT_ARROW, "&lt;", result);
200         result = regexReplace(P_RIGHT_ARROW, "&gt;", result);
201         return result;
202     }
203
204     //---------------------------------------------------------------
205     /**
206      * given a user submitted input String, filter out any invalid or restricted
207      * html.
208      *
209      * @param input text (i.e. submitted by a user) than may contain html
210      * @return "clean" version of input, with only valid, whitelisted html elements allowed
211      */
212     public String filter(final String input) {
213         reset();
214         String s = input;
215
216         debug("************************************************");
217         debug("              INPUT: " + input);
218
219         s = escapeComments(s);
220         debug("     escapeComments: " + s);
221
222         s = balanceHTML(s);
223         debug("        balanceHTML: " + s);
224
225         s = checkTags(s);
226         debug("          checkTags: " + s);
227
228         s = processRemoveBlanks(s);
229         debug("processRemoveBlanks: " + s);
230
231         s = validateEntities(s);
232         debug("    validateEntites: " + s);
233
234         debug("************************************************\n\n");
235         return s;
236     }
237
238     public boolean isAlwaysMakeTags(){
239         return alwaysMakeTags;
240     }
241
242     public boolean isStripComments(){
243         return stripComment;
244     }
245
246     private String escapeComments(final String s) {
247         final Matcher m = P_COMMENTS.matcher(s);
248         final StringBuffer buf = new StringBuffer();
249         if (m.find()) {
250             final String match = m.group(1); //(.*?)
251             m.appendReplacement(buf, Matcher.quoteReplacement("<!--" + htmlSpecialChars(match) + "-->"));
252         }
253         m.appendTail(buf);
254
255         return buf.toString();
256     }
257
258     private String balanceHTML(String s) {
259         if (alwaysMakeTags) {
260             //
261             // try and form html
262             //
263             s = regexReplace(P_END_ARROW, "", s);
264             s = regexReplace(P_BODY_TO_END, "<$1>", s);
265             s = regexReplace(P_XML_CONTENT, "$1<$2", s);
266
267         } else {
268             //
269             // escape stray brackets
270             //
271             s = regexReplace(P_STRAY_LEFT_ARROW, "&lt;$1", s);
272             s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2&gt;<", s);
273
274             //
275             // the last regexp causes '<>' entities to appear
276             // (we need to do a lookahead assertion so that the last bracket can
277             // be used in the next pass of the regexp)
278             //
279             s = regexReplace(P_BOTH_ARROWS, "", s);
280         }
281
282         return s;
283     }
284
285     private String checkTags(String s) {
286         Matcher m = P_TAGS.matcher(s);
287
288         final StringBuffer buf = new StringBuffer();
289         while (m.find()) {
290             String replaceStr = m.group(1);
291             replaceStr = processTag(replaceStr);
292             m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr));
293         }
294         m.appendTail(buf);
295
296         s = buf.toString();
297
298         // these get tallied in processTag
299         // (remember to reset before subsequent calls to filter method)
300         for (String key : vTagCounts.keySet()) {
301             for (int ii = 0; ii < vTagCounts.get(key); ii++) {
302                 s += "</" + key + ">";
303             }
304         }
305
306         return s;
307     }
308
309     private String processRemoveBlanks(final String s) {
310         String result = s;
311         for (String tag : vRemoveBlanks) {
312             if(!P_REMOVE_PAIR_BLANKS.containsKey(tag)){
313                 P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?></" + tag + ">"));
314             }
315             result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result);
316             if(!P_REMOVE_SELF_BLANKS.containsKey(tag)){
317                 P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>"));
318             }
319             result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result);
320         }
321
322         return result;
323     }
324
325     private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) {
326         Matcher m = regex_pattern.matcher(s);
327         return m.replaceAll(replacement);
328     }
329
330     private String processTag(final String s) {
331         // ending tags
332         Matcher m = P_END_TAG.matcher(s);
333         if (m.find()) {
334             final String name = m.group(1).toLowerCase();
335             if (allowed(name)) {
336                 if (!inArray(name, vSelfClosingTags)) {
337                     if (vTagCounts.containsKey(name)) {
338                         vTagCounts.put(name, vTagCounts.get(name) - 1);
339                         return "</" + name + ">";
340                     }
341                 }
342             }
343         }
344
345         // starting tags
346         m = P_START_TAG.matcher(s);
347         if (m.find()) {
348             final String name = m.group(1).toLowerCase();
349             final String body = m.group(2);
350             String ending = m.group(3);
351
352             //debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" );
353             if (allowed(name)) {
354                 String params = "";
355
356                 final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body);
357                 final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body);
358                 final List<String> paramNames = new ArrayList<String>();
359                 final List<String> paramValues = new ArrayList<String>();
360                 while (m2.find()) {
361                     paramNames.add(m2.group(1)); //([a-z0-9]+)
362                     paramValues.add(m2.group(3)); //(.*?)
363                 }
364                 while (m3.find()) {
365                     paramNames.add(m3.group(1)); //([a-z0-9]+)
366                     paramValues.add(m3.group(3)); //([^\"\\s']+)
367                 }
368
369                 String paramName, paramValue;
370                 for (int ii = 0; ii < paramNames.size(); ii++) {
371                     paramName = paramNames.get(ii).toLowerCase();
372                     paramValue = paramValues.get(ii);
373
374 //          debug( "paramName='" + paramName + "'" );
375 //          debug( "paramValue='" + paramValue + "'" );
376 //          debug( "allowed? " + vAllowed.get( name ).contains( paramName ) );
377
378                     if (allowedAttribute(name, paramName)) {
379                         if (inArray(paramName, vProtocolAtts)) {
380                             paramValue = processParamProtocol(paramValue);
381                         }
382                         params += " " + paramName + "=\"" + paramValue + "\"";
383                     }
384                 }
385
386                 if (inArray(name, vSelfClosingTags)) {
387                     ending = " /";
388                 }
389
390                 if (inArray(name, vNeedClosingTags)) {
391                     ending = "";
392                 }
393
394                 if (ending == null || ending.length() < 1) {
395                     if (vTagCounts.containsKey(name)) {
396                         vTagCounts.put(name, vTagCounts.get(name) + 1);
397                     } else {
398                         vTagCounts.put(name, 1);
399                     }
400                 } else {
401                     ending = " /";
402                 }
403                 return "<" + name + params + ending + ">";
404             } else {
405                 return "";
406             }
407         }
408
409         // comments
410         m = P_COMMENT.matcher(s);
411         if (!stripComment && m.find()) {
412             return  "<" + m.group() + ">";
413         }
414
415         return "";
416     }
417
418     private String processParamProtocol(String s) {
419         s = decodeEntities(s);
420         final Matcher m = P_PROTOCOL.matcher(s);
421         if (m.find()) {
422             final String protocol = m.group(1);
423             if (!inArray(protocol, vAllowedProtocols)) {
424                 // bad protocol, turn into local anchor link instead
425                 s = "#" + s.substring(protocol.length() + 1, s.length());
426                 if (s.startsWith("#//")) {
427                     s = "#" + s.substring(3, s.length());
428                 }
429             }
430         }
431
432         return s;
433     }
434
435     private String decodeEntities(String s) {
436         StringBuffer buf = new StringBuffer();
437
438         Matcher m = P_ENTITY.matcher(s);
439         while (m.find()) {
440             final String match = m.group(1);
441             final int decimal = Integer.decode(match).intValue();
442             m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
443         }
444         m.appendTail(buf);
445         s = buf.toString();
446
447         buf = new StringBuffer();
448         m = P_ENTITY_UNICODE.matcher(s);
449         while (m.find()) {
450             final String match = m.group(1);
451             final int decimal = Integer.valueOf(match, 16).intValue();
452             m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
453         }
454         m.appendTail(buf);
455         s = buf.toString();
456
457         buf = new StringBuffer();
458         m = P_ENCODE.matcher(s);
459         while (m.find()) {
460             final String match = m.group(1);
461             final int decimal = Integer.valueOf(match, 16).intValue();
462             m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
463         }
464         m.appendTail(buf);
465         s = buf.toString();
466
467         s = validateEntities(s);
468         return s;
469     }
470
471     private String validateEntities(final String s) {
472         StringBuffer buf = new StringBuffer();
473
474         // validate entities throughout the string
475         Matcher m = P_VALID_ENTITIES.matcher(s);
476         while (m.find()) {
477             final String one = m.group(1); //([^&;]*)
478             final String two = m.group(2); //(?=(;|&|$))
479             m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two)));
480         }
481         m.appendTail(buf);
482
483         return encodeQuotes(buf.toString());
484     }
485
486     private String encodeQuotes(final String s){
487         if(encodeQuotes){
488             StringBuffer buf = new StringBuffer();
489             Matcher m = P_VALID_QUOTES.matcher(s);
490             while (m.find()) {
491                 final String one = m.group(1); //(>|^)
492                 final String two = m.group(2); //([^<]+?)
493                 final String three = m.group(3); //(<|$)
494                 m.appendReplacement(buf, Matcher.quoteReplacement(one + regexReplace(P_QUOTE, "&quot;", two) + three));
495             }
496             m.appendTail(buf);
497             return buf.toString();
498         }else{
499             return s;
500         }
501     }
502
503     private String checkEntity(final String preamble, final String term) {
504
505         return ";".equals(term) && isValidEntity(preamble)
506                 ? '&' + preamble
507                 : "&amp;" + preamble;
508     }
509
510     private boolean isValidEntity(final String entity) {
511         return inArray(entity, vAllowedEntities);
512     }
513
514     private static boolean inArray(final String s, final String[] array) {
515         for (String item : array) {
516             if (item != null && item.equals(s)) {
517                 return true;
518             }
519         }
520         return false;
521     }
522
523     private boolean allowed(final String name) {
524         return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed);
525     }
526
527     private boolean allowedAttribute(final String name, final String paramName) {
528         return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName));
529     }
530 }