Pārlūkot izejas kodu

支持配置XSS跨站脚本过滤

RuoYi 4 gadi atpakaļ
vecāks
revīzija
954d208ac6

+ 155 - 0
ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/html/EscapeUtil.java

@@ -0,0 +1,155 @@
1
+package com.ruoyi.common.core.utils.html;
2
+
3
+import com.ruoyi.common.core.utils.StringUtils;
4
+
5
+/**
6
+ * 转义和反转义工具类
7
+ * 
8
+ * @author ruoyi
9
+ */
10
+public class EscapeUtil
11
+{
12
+    public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)";
13
+
14
+    private static final char[][] TEXT = new char[64][];
15
+
16
+    static
17
+    {
18
+        for (int i = 0; i < 64; i++)
19
+        {
20
+            TEXT[i] = new char[] { (char) i };
21
+        }
22
+
23
+        // special HTML characters
24
+        TEXT['\''] = "&#039;".toCharArray(); // 单引号
25
+        TEXT['"'] = "&#34;".toCharArray(); // 双引号
26
+        TEXT['&'] = "&#38;".toCharArray(); // &符
27
+        TEXT['<'] = "&#60;".toCharArray(); // 小于号
28
+        TEXT['>'] = "&#62;".toCharArray(); // 大于号
29
+    }
30
+
31
+    /**
32
+     * 转义文本中的HTML字符为安全的字符
33
+     * 
34
+     * @param text 被转义的文本
35
+     * @return 转义后的文本
36
+     */
37
+    public static String escape(String text)
38
+    {
39
+        return encode(text);
40
+    }
41
+
42
+    /**
43
+     * 还原被转义的HTML特殊字符
44
+     * 
45
+     * @param content 包含转义符的HTML内容
46
+     * @return 转换后的字符串
47
+     */
48
+    public static String unescape(String content)
49
+    {
50
+        return decode(content);
51
+    }
52
+
53
+    /**
54
+     * 清除所有HTML标签,但是不删除标签内的内容
55
+     * 
56
+     * @param content 文本
57
+     * @return 清除标签后的文本
58
+     */
59
+    public static String clean(String content)
60
+    {
61
+        return new HTMLFilter().filter(content);
62
+    }
63
+
64
+    /**
65
+     * Escape编码
66
+     * 
67
+     * @param text 被编码的文本
68
+     * @return 编码后的字符
69
+     */
70
+    private static String encode(String text)
71
+    {
72
+        int len;
73
+        if ((text == null) || ((len = text.length()) == 0))
74
+        {
75
+            return StringUtils.EMPTY;
76
+        }
77
+        StringBuilder buffer = new StringBuilder(len + (len >> 2));
78
+        char c;
79
+        for (int i = 0; i < len; i++)
80
+        {
81
+            c = text.charAt(i);
82
+            if (c < 64)
83
+            {
84
+                buffer.append(TEXT[c]);
85
+            }
86
+            else
87
+            {
88
+                buffer.append(c);
89
+            }
90
+        }
91
+        return buffer.toString();
92
+    }
93
+
94
+    /**
95
+     * Escape解码
96
+     * 
97
+     * @param content 被转义的内容
98
+     * @return 解码后的字符串
99
+     */
100
+    public static String decode(String content)
101
+    {
102
+        if (StringUtils.isEmpty(content))
103
+        {
104
+            return content;
105
+        }
106
+
107
+        StringBuilder tmp = new StringBuilder(content.length());
108
+        int lastPos = 0, pos = 0;
109
+        char ch;
110
+        while (lastPos < content.length())
111
+        {
112
+            pos = content.indexOf("%", lastPos);
113
+            if (pos == lastPos)
114
+            {
115
+                if (content.charAt(pos + 1) == 'u')
116
+                {
117
+                    ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
118
+                    tmp.append(ch);
119
+                    lastPos = pos + 6;
120
+                }
121
+                else
122
+                {
123
+                    ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);
124
+                    tmp.append(ch);
125
+                    lastPos = pos + 3;
126
+                }
127
+            }
128
+            else
129
+            {
130
+                if (pos == -1)
131
+                {
132
+                    tmp.append(content.substring(lastPos));
133
+                    lastPos = content.length();
134
+                }
135
+                else
136
+                {
137
+                    tmp.append(content.substring(lastPos, pos));
138
+                    lastPos = pos;
139
+                }
140
+            }
141
+        }
142
+        return tmp.toString();
143
+    }
144
+
145
+    public static void main(String[] args)
146
+    {
147
+        String html = "<script>alert(1);</script>";
148
+        // String html = "<scr<script>ipt>alert(\"XSS\")</scr<script>ipt>";
149
+        // String html = "<123";
150
+        // String html = "123>";
151
+        System.out.println(EscapeUtil.clean(html));
152
+        System.out.println(EscapeUtil.escape(html));
153
+        System.out.println(EscapeUtil.unescape(html));
154
+    }
155
+}

+ 570 - 0
ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/html/HTMLFilter.java

@@ -0,0 +1,570 @@
1
+package com.ruoyi.common.core.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 ruoyi
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 (false == 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
+}

+ 3 - 3
ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/properties/CaptchaProperties.java

@@ -17,19 +17,19 @@ public class CaptchaProperties
17 17
     /**
18 18
      * 验证码开关
19 19
      */
20
-    private boolean enabled;
20
+    private Boolean enabled;
21 21
 
22 22
     /**
23 23
      * 验证码类型(math 数组计算 char 字符)
24 24
      */
25 25
     private String type;
26 26
 
27
-    public boolean isEnabled()
27
+    public Boolean getEnabled()
28 28
     {
29 29
         return enabled;
30 30
     }
31 31
 
32
-    public void setEnabled(boolean enabled)
32
+    public void setEnabled(Boolean enabled)
33 33
     {
34 34
         this.enabled = enabled;
35 35
     }

+ 1 - 1
ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/properties/IgnoreWhiteProperties.java

@@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration;
13 13
  */
14 14
 @Configuration
15 15
 @RefreshScope
16
-@ConfigurationProperties(prefix = "ignore")
16
+@ConfigurationProperties(prefix = "security.ignore")
17 17
 public class IgnoreWhiteProperties
18 18
 {
19 19
     /**

+ 48 - 0
ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/properties/XssProperties.java

@@ -0,0 +1,48 @@
1
+package com.ruoyi.gateway.config.properties;
2
+
3
+import java.util.ArrayList;
4
+import java.util.List;
5
+import org.springframework.boot.context.properties.ConfigurationProperties;
6
+import org.springframework.cloud.context.config.annotation.RefreshScope;
7
+import org.springframework.context.annotation.Configuration;
8
+
9
+/**
10
+ * XSS跨站脚本配置
11
+ * 
12
+ * @author ruoyi
13
+ */
14
+@Configuration
15
+@RefreshScope
16
+@ConfigurationProperties(prefix = "security.xss")
17
+public class XssProperties
18
+{
19
+    /**
20
+     * Xss开关
21
+     */
22
+    private Boolean enabled;
23
+
24
+    /**
25
+     * 排除路径
26
+     */
27
+    private List<String> excludeUrls = new ArrayList<>();
28
+
29
+    public Boolean getEnabled()
30
+    {
31
+        return enabled;
32
+    }
33
+
34
+    public void setEnabled(Boolean enabled)
35
+    {
36
+        this.enabled = enabled;
37
+    }
38
+
39
+    public List<String> getExcludeUrls()
40
+    {
41
+        return excludeUrls;
42
+    }
43
+
44
+    public void setExcludeUrls(List<String> excludeUrls)
45
+    {
46
+        this.excludeUrls = excludeUrls;
47
+    }
48
+}

+ 5 - 0
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/CacheRequestFilter.java

@@ -16,6 +16,11 @@ import org.springframework.web.server.ServerWebExchange;
16 16
 import reactor.core.publisher.Flux;
17 17
 import reactor.core.publisher.Mono;
18 18
 
19
+/**
20
+ * 获取body请求数据(解决流不能重复读取问题)
21
+ * 
22
+ * @author ruoyi
23
+ */
19 24
 @Component
20 25
 public class CacheRequestFilter extends AbstractGatewayFilterFactory<CacheRequestFilter.Config>
21 26
 {

+ 1 - 1
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/ValidateCodeFilter.java

@@ -47,7 +47,7 @@ public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object>
47 47
             ServerHttpRequest request = exchange.getRequest();
48 48
 
49 49
             // 非登录请求或验证码关闭,不处理
50
-            if (!StringUtils.containsIgnoreCase(request.getURI().getPath(), AUTH_URL) || !captchaProperties.isEnabled())
50
+            if (!StringUtils.containsIgnoreCase(request.getURI().getPath(), AUTH_URL) || !captchaProperties.getEnabled())
51 51
             {
52 52
                 return chain.filter(exchange);
53 53
             }

+ 101 - 0
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/XssFilter.java

@@ -0,0 +1,101 @@
1
+package com.ruoyi.gateway.filter;
2
+
3
+import java.nio.charset.StandardCharsets;
4
+import org.springframework.beans.factory.annotation.Autowired;
5
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
6
+import org.springframework.cloud.gateway.filter.GlobalFilter;
7
+import org.springframework.core.Ordered;
8
+import org.springframework.core.io.buffer.DataBuffer;
9
+import org.springframework.core.io.buffer.DataBufferUtils;
10
+import org.springframework.core.io.buffer.NettyDataBufferFactory;
11
+import org.springframework.http.HttpHeaders;
12
+import org.springframework.http.HttpMethod;
13
+import org.springframework.http.server.reactive.ServerHttpRequest;
14
+import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
15
+import org.springframework.stereotype.Component;
16
+import org.springframework.web.server.ServerWebExchange;
17
+import com.ruoyi.common.core.utils.StringUtils;
18
+import com.ruoyi.common.core.utils.html.EscapeUtil;
19
+import com.ruoyi.gateway.config.properties.XssProperties;
20
+import io.netty.buffer.ByteBufAllocator;
21
+import reactor.core.publisher.Flux;
22
+import reactor.core.publisher.Mono;
23
+
24
+/**
25
+ * 跨站脚本过滤器
26
+ *
27
+ * @author ruoyi
28
+ */
29
+@Component
30
+public class XssFilter implements GlobalFilter, Ordered
31
+{
32
+    // 跨站脚本的 xss 配置,nacos自行添加
33
+    @Autowired
34
+    private XssProperties xss;
35
+
36
+    @Override
37
+    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
38
+    {
39
+        ServerHttpRequest request = exchange.getRequest();
40
+        // GET DELETE 不过滤
41
+        HttpMethod method = request.getMethod();
42
+        if (method == null || method.matches("GET") || method.matches("DELETE"))
43
+        {
44
+            return chain.filter(exchange);
45
+        }
46
+        // excludeUrls 不过滤
47
+        String url = request.getURI().getPath();
48
+        if (StringUtils.matches(url, xss.getExcludeUrls()))
49
+        {
50
+            return chain.filter(exchange);
51
+        }
52
+        ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
53
+        return chain.filter(exchange.mutate().request(httpRequestDecorator).build());
54
+
55
+    }
56
+
57
+    private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange)
58
+    {
59
+        ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest())
60
+        {
61
+            @Override
62
+            public Flux<DataBuffer> getBody()
63
+            {
64
+                Flux<DataBuffer> body = super.getBody();
65
+                return body.map(dataBuffer -> {
66
+                    byte[] content = new byte[dataBuffer.readableByteCount()];
67
+                    dataBuffer.read(content);
68
+                    DataBufferUtils.release(dataBuffer);
69
+                    String bodyStr = new String(content, StandardCharsets.UTF_8);
70
+                    // 防xss攻击过滤
71
+                    bodyStr = EscapeUtil.clean(bodyStr);
72
+                    // 转成字节
73
+                    byte[] bytes = bodyStr.getBytes();
74
+                    NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
75
+                    DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
76
+                    buffer.write(bytes);
77
+                    return buffer;
78
+                });
79
+            }
80
+
81
+            @Override
82
+            public HttpHeaders getHeaders()
83
+            {
84
+                HttpHeaders httpHeaders = new HttpHeaders();
85
+                httpHeaders.putAll(super.getHeaders());
86
+                // 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length
87
+                httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
88
+                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
89
+                return httpHeaders;
90
+            }
91
+
92
+        };
93
+        return serverHttpRequestDecorator;
94
+    }
95
+
96
+    @Override
97
+    public int getOrder()
98
+    {
99
+        return -100;
100
+    }
101
+}

+ 1 - 1
ruoyi-gateway/src/main/java/com/ruoyi/gateway/service/impl/ValidateCodeServiceImpl.java

@@ -46,7 +46,7 @@ public class ValidateCodeServiceImpl implements ValidateCodeService
46 46
     public AjaxResult createCapcha() throws IOException, CaptchaException
47 47
     {
48 48
         AjaxResult ajax = AjaxResult.success();
49
-        boolean captchaOnOff = captchaProperties.isEnabled();
49
+        boolean captchaOnOff = captchaProperties.getEnabled();
50 50
         ajax.put("captchaOnOff", captchaOnOff);
51 51
         if (!captchaOnOff)
52 52
         {

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
sql/ry_config_20210727.sql