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