init
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>BladeX-Tool</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>blade-starter-xss</artifactId>
|
||||
<name>${project.artifactId}</name>
|
||||
<version>${project.parent.version}</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<module.name>org.springblade.blade.core.xss</module.name>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-core-tool</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
</dependency>
|
||||
<!-- Auto -->
|
||||
<dependency>
|
||||
<groupId>org.springblade</groupId>
|
||||
<artifactId>blade-core-auto</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,385 @@
|
||||
package org.jsoup.nodes;
|
||||
|
||||
import org.jsoup.SerializationException;
|
||||
import org.jsoup.helper.Validate;
|
||||
import org.jsoup.internal.StringUtil;
|
||||
import org.jsoup.nodes.Document.OutputSettings;
|
||||
import org.jsoup.parser.CharacterReader;
|
||||
import org.jsoup.parser.Parser;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
||||
import static org.jsoup.nodes.Document.OutputSettings.Syntax;
|
||||
import static org.jsoup.nodes.Entities.EscapeMode.base;
|
||||
import static org.jsoup.nodes.Entities.EscapeMode.extended;
|
||||
|
||||
/**
|
||||
* HTML entities, and escape routines. Source: <a href="http://www.w3.org/TR/html5/named-character-references.html#named-character-references">W3C
|
||||
* HTML named character references</a>.
|
||||
*/
|
||||
public class Entities {
|
||||
private static final int empty = -1;
|
||||
private static final String emptyName = "";
|
||||
static final int codepointRadix = 36;
|
||||
private static final char[] codeDelims = {',', ';'};
|
||||
private static final HashMap<String, String> multipoints = new HashMap<>(); // name -> multiple character references
|
||||
|
||||
public enum EscapeMode {
|
||||
/**
|
||||
* Restricted entities suitable for XHTML output: lt, gt, amp, and quot only.
|
||||
*/
|
||||
xhtml(EntitiesData.xmlPoints, 4),
|
||||
/**
|
||||
* Default HTML output entities.
|
||||
*/
|
||||
base(EntitiesData.basePoints, 106),
|
||||
/**
|
||||
* Complete HTML entities.
|
||||
*/
|
||||
extended(EntitiesData.fullPoints, 2125);
|
||||
|
||||
// table of named references to their codepoints. sorted so we can binary search. built by BuildEntities.
|
||||
private String[] nameKeys;
|
||||
private int[] codeVals; // limitation is the few references with multiple characters; those go into multipoints.
|
||||
|
||||
// table of codepoints to named entities.
|
||||
private int[] codeKeys; // we don't support multicodepoints to single named value currently
|
||||
private String[] nameVals;
|
||||
|
||||
EscapeMode(String file, int size) {
|
||||
load(this, file, size);
|
||||
}
|
||||
|
||||
int codepointForName(final String name) {
|
||||
int index = Arrays.binarySearch(nameKeys, name);
|
||||
return index >= 0 ? codeVals[index] : empty;
|
||||
}
|
||||
|
||||
String nameForCodepoint(final int codepoint) {
|
||||
final int index = Arrays.binarySearch(codeKeys, codepoint);
|
||||
if (index >= 0) {
|
||||
// the results are ordered so lower case versions of same codepoint come after uppercase, and we prefer to emit lower
|
||||
// (and binary search for same item with multi results is undefined
|
||||
return (index < nameVals.length - 1 && codeKeys[index + 1] == codepoint) ?
|
||||
nameVals[index + 1] : nameVals[index];
|
||||
}
|
||||
return emptyName;
|
||||
}
|
||||
|
||||
private int size() {
|
||||
return nameKeys.length;
|
||||
}
|
||||
}
|
||||
|
||||
private Entities() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the input is a known named entity
|
||||
*
|
||||
* @param name the possible entity name (e.g. "lt" or "amp")
|
||||
* @return true if a known named entity
|
||||
*/
|
||||
public static boolean isNamedEntity(final String name) {
|
||||
return extended.codepointForName(name) != empty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the input is a known named entity in the base entity set.
|
||||
*
|
||||
* @param name the possible entity name (e.g. "lt" or "amp")
|
||||
* @return true if a known named entity in the base set
|
||||
* @see #isNamedEntity(String)
|
||||
*/
|
||||
public static boolean isBaseNamedEntity(final String name) {
|
||||
return base.codepointForName(name) != empty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the character(s) represented by the named entity
|
||||
*
|
||||
* @param name entity (e.g. "lt" or "amp")
|
||||
* @return the string value of the character(s) represented by this entity, or "" if not defined
|
||||
*/
|
||||
public static String getByName(String name) {
|
||||
String val = multipoints.get(name);
|
||||
if (val != null)
|
||||
return val;
|
||||
int codepoint = extended.codepointForName(name);
|
||||
if (codepoint != empty)
|
||||
return new String(new int[]{codepoint}, 0, 1);
|
||||
return emptyName;
|
||||
}
|
||||
|
||||
public static int codepointsForName(final String name, final int[] codepoints) {
|
||||
String val = multipoints.get(name);
|
||||
if (val != null) {
|
||||
codepoints[0] = val.codePointAt(0);
|
||||
codepoints[1] = val.codePointAt(1);
|
||||
return 2;
|
||||
}
|
||||
int codepoint = extended.codepointForName(name);
|
||||
if (codepoint != empty) {
|
||||
codepoints[0] = codepoint;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
HTML escape an input string. That is, {@code <} is returned as {@code <}. The escaped string is suitable for use
|
||||
both in attributes and in text data.
|
||||
@param string the un-escaped string to escape
|
||||
@param out the output settings to use. This configures the character set escaped against (that is, if a
|
||||
character is supported in the output character set, it doesn't have to be escaped), and also HTML or XML
|
||||
settings.
|
||||
@return the escaped string
|
||||
*/
|
||||
public static String escape(String string, OutputSettings out) {
|
||||
if (string == null)
|
||||
return "";
|
||||
StringBuilder accum = StringUtil.borrowBuilder();
|
||||
try {
|
||||
escape(accum, string, out, true, true, false, false, false); // for text and for attribute; preserve whitespaces
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException(e); // doesn't happen
|
||||
}
|
||||
return StringUtil.releaseBuilder(accum);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML escape an input string, using the default settings (UTF-8, base entities). That is, {@code <} is returned as
|
||||
* {@code <}. The escaped string is suitable for use both in attributes and in text data.
|
||||
*
|
||||
* @param string the un-escaped string to escape
|
||||
* @return the escaped string
|
||||
* @see #escape(String, OutputSettings)
|
||||
*/
|
||||
public static String escape(String string) {
|
||||
if (DefaultOutput == null)
|
||||
DefaultOutput = new OutputSettings();
|
||||
return escape(string, DefaultOutput);
|
||||
}
|
||||
private static @Nullable OutputSettings DefaultOutput; // lazy-init, to break circular dependency with OutputSettings
|
||||
|
||||
// this method does a lot, but other breakups cause rescanning and stringbuilder generations
|
||||
static void escape(Appendable accum, String string, OutputSettings out,
|
||||
boolean forText, boolean forAttribute, boolean normaliseWhite, boolean stripLeadingWhite, boolean trimTrailing) throws IOException {
|
||||
|
||||
boolean lastWasWhite = false;
|
||||
boolean reachedNonWhite = false;
|
||||
final EscapeMode escapeMode = out.escapeMode();
|
||||
final CharsetEncoder encoder = out.encoder();
|
||||
final CoreCharset coreCharset = out.coreCharset; // init in out.prepareEncoder()
|
||||
final int length = string.length();
|
||||
|
||||
int codePoint;
|
||||
boolean skipped = false;
|
||||
for (int offset = 0; offset < length; offset += Character.charCount(codePoint)) {
|
||||
codePoint = string.codePointAt(offset);
|
||||
|
||||
if (normaliseWhite) {
|
||||
if (StringUtil.isWhitespace(codePoint)) {
|
||||
if (stripLeadingWhite && !reachedNonWhite) continue;
|
||||
if (lastWasWhite) continue;
|
||||
if (trimTrailing) {
|
||||
skipped = true;
|
||||
continue;
|
||||
}
|
||||
accum.append(' ');
|
||||
lastWasWhite = true;
|
||||
continue;
|
||||
} else {
|
||||
lastWasWhite = false;
|
||||
reachedNonWhite = true;
|
||||
if (skipped) {
|
||||
accum.append(' '); // wasn't the end, so need to place a normalized space
|
||||
skipped = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// surrogate pairs, split implementation for efficiency on single char common case (saves creating strings, char[]):
|
||||
if (codePoint < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
|
||||
final char c = (char) codePoint;
|
||||
// html specific and required escapes:
|
||||
switch (c) {
|
||||
// BladeX 针对 & 符号转换为 & 的处理,若外部传入非 EscapeMode.extended 则取消转换逻辑
|
||||
// 例如:XssUtil.clean("&233&", new Document.OutputSettings().escapeMode(Entities.EscapeMode.extended))
|
||||
case '&':
|
||||
if (escapeMode == EscapeMode.extended)
|
||||
accum.append("&");
|
||||
else
|
||||
accum.append(c);
|
||||
break;
|
||||
case 0xA0:
|
||||
if (escapeMode != EscapeMode.xhtml)
|
||||
accum.append(" ");
|
||||
else
|
||||
accum.append(" ");
|
||||
break;
|
||||
case '<':
|
||||
// escape when in character data or when in a xml attribute val or XML syntax; not needed in html attr val
|
||||
if (forText || escapeMode == EscapeMode.xhtml || out.syntax() == Syntax.xml)
|
||||
accum.append("<");
|
||||
else
|
||||
accum.append(c);
|
||||
break;
|
||||
case '>':
|
||||
if (forText)
|
||||
accum.append(">");
|
||||
else
|
||||
accum.append(c);
|
||||
break;
|
||||
case '"':
|
||||
if (forAttribute)
|
||||
accum.append(""");
|
||||
else
|
||||
accum.append(c);
|
||||
break;
|
||||
case '\'':
|
||||
if (forAttribute && forText) { // special case for the Entities.escape(string) method when we are maximally escaping. Otherwise, because we output attributes in "", there's no need to escape.
|
||||
if (escapeMode == EscapeMode.xhtml)
|
||||
accum.append("'");
|
||||
else
|
||||
accum.append("'");
|
||||
}
|
||||
else
|
||||
accum.append(c);
|
||||
break;
|
||||
// we escape ascii control <x20 (other than tab, line-feed, carriage return) for XML compliance (required) and HTML ease of reading (not required) - https://www.w3.org/TR/xml/#charsets
|
||||
case 0x9:
|
||||
case 0xA:
|
||||
case 0xD:
|
||||
accum.append(c);
|
||||
break;
|
||||
default:
|
||||
if (c < 0x20 || !canEncode(coreCharset, c, encoder))
|
||||
appendEncoded(accum, escapeMode, codePoint);
|
||||
else
|
||||
accum.append(c);
|
||||
}
|
||||
} else {
|
||||
final String c = new String(Character.toChars(codePoint));
|
||||
if (encoder.canEncode(c)) // uses fallback encoder for simplicity
|
||||
accum.append(c);
|
||||
else
|
||||
appendEncoded(accum, escapeMode, codePoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendEncoded(Appendable accum, EscapeMode escapeMode, int codePoint) throws IOException {
|
||||
final String name = escapeMode.nameForCodepoint(codePoint);
|
||||
if (!emptyName.equals(name)) // ok for identity check
|
||||
accum.append('&').append(name).append(';');
|
||||
else
|
||||
accum.append("&#x").append(Integer.toHexString(codePoint)).append(';');
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-escape an HTML escaped string. That is, {@code <} is returned as {@code <}.
|
||||
*
|
||||
* @param string the HTML string to un-escape
|
||||
* @return the unescaped string
|
||||
*/
|
||||
public static String unescape(String string) {
|
||||
return unescape(string, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescape the input string.
|
||||
*
|
||||
* @param string to un-HTML-escape
|
||||
* @param strict if "strict" (that is, requires trailing ';' char, otherwise that's optional)
|
||||
* @return unescaped string
|
||||
*/
|
||||
static String unescape(String string, boolean strict) {
|
||||
return Parser.unescapeEntities(string, strict);
|
||||
}
|
||||
|
||||
/*
|
||||
* Provides a fast-path for Encoder.canEncode, which drastically improves performance on Android post JellyBean.
|
||||
* After KitKat, the implementation of canEncode degrades to the point of being useless. For non ASCII or UTF,
|
||||
* performance may be bad. We can add more encoders for common character sets that are impacted by performance
|
||||
* issues on Android if required.
|
||||
*
|
||||
* Benchmarks: *
|
||||
* OLD toHtml() impl v New (fastpath) in millis
|
||||
* Wiki: 1895, 16
|
||||
* CNN: 6378, 55
|
||||
* Alterslash: 3013, 28
|
||||
* Jsoup: 167, 2
|
||||
*/
|
||||
private static boolean canEncode(final CoreCharset charset, final char c, final CharsetEncoder fallback) {
|
||||
// todo add more charset tests if impacted by Android's bad perf in canEncode
|
||||
switch (charset) {
|
||||
case ascii:
|
||||
return c < 0x80;
|
||||
case utf:
|
||||
return true; // real is:!(Character.isLowSurrogate(c) || Character.isHighSurrogate(c)); - but already check above
|
||||
default:
|
||||
return fallback.canEncode(c);
|
||||
}
|
||||
}
|
||||
|
||||
enum CoreCharset {
|
||||
ascii, utf, fallback;
|
||||
|
||||
static CoreCharset byName(final String name) {
|
||||
if (name.equals("US-ASCII"))
|
||||
return ascii;
|
||||
if (name.startsWith("UTF-")) // covers UTF-8, UTF-16, et al
|
||||
return utf;
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static void load(EscapeMode e, String pointsData, int size) {
|
||||
e.nameKeys = new String[size];
|
||||
e.codeVals = new int[size];
|
||||
e.codeKeys = new int[size];
|
||||
e.nameVals = new String[size];
|
||||
|
||||
int i = 0;
|
||||
CharacterReader reader = new CharacterReader(pointsData);
|
||||
try {
|
||||
while (!reader.isEmpty()) {
|
||||
// NotNestedLessLess=10913,824;1887&
|
||||
|
||||
final String name = reader.consumeTo('=');
|
||||
reader.advance();
|
||||
final int cp1 = Integer.parseInt(reader.consumeToAny(codeDelims), codepointRadix);
|
||||
final char codeDelim = reader.current();
|
||||
reader.advance();
|
||||
final int cp2;
|
||||
if (codeDelim == ',') {
|
||||
cp2 = Integer.parseInt(reader.consumeTo(';'), codepointRadix);
|
||||
reader.advance();
|
||||
} else {
|
||||
cp2 = empty;
|
||||
}
|
||||
final String indexS = reader.consumeTo('&');
|
||||
final int index = Integer.parseInt(indexS, codepointRadix);
|
||||
reader.advance();
|
||||
|
||||
e.nameKeys[i] = name;
|
||||
e.codeVals[i] = cp1;
|
||||
e.codeKeys[index] = cp1;
|
||||
e.nameVals[index] = name;
|
||||
|
||||
if (cp2 != empty) {
|
||||
multipoints.put(name, new String(new int[]{cp1, cp2}, 0, 2));
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
Validate.isTrue(i == size, "Unexpected count of entities loaded");
|
||||
} finally {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Xss配置类
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@RefreshScope
|
||||
@ConfigurationProperties(BladeXssProperties.PREFIX)
|
||||
public class BladeXssProperties {
|
||||
public static final String PREFIX = "blade.xss";
|
||||
|
||||
/**
|
||||
* 开启xss
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
/**
|
||||
* 全局:对文件进行首尾 trim
|
||||
*/
|
||||
private boolean trimText = true;
|
||||
/**
|
||||
* 模式:clear 清理(默认),escape 转义
|
||||
*/
|
||||
private Mode mode = Mode.CLEAR;
|
||||
/**
|
||||
* [clear 专用] prettyPrint,默认关闭: 保留换行
|
||||
*/
|
||||
private boolean prettyPrint = false;
|
||||
/**
|
||||
* [clear 专用] 使用转义,默认关闭
|
||||
*/
|
||||
private boolean enableEscape = false;
|
||||
/**
|
||||
* 拦截的路由,默认为所有
|
||||
*/
|
||||
private List<String> blockUrl = Collections.singletonList("/**");
|
||||
/**
|
||||
* 放行的路由,默认为空
|
||||
*/
|
||||
private List<String> skipUrl = new ArrayList<>();
|
||||
|
||||
public enum Mode {
|
||||
/**
|
||||
* 清理
|
||||
*/
|
||||
CLEAR,
|
||||
/**
|
||||
* 转义
|
||||
*/
|
||||
ESCAPE,
|
||||
/**
|
||||
* 校验,抛出异常
|
||||
*/
|
||||
VALIDATE
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 利用 ThreadLocal 缓存线程间的数据
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@UtilityClass
|
||||
public class XssHolder {
|
||||
private static final ThreadLocal<XssIgnoreRules> TL = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isEnabled() {
|
||||
return Objects.isNull(TL.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否被忽略
|
||||
*
|
||||
* @return XssCleanIgnore
|
||||
*/
|
||||
public static boolean isIgnore(String name) {
|
||||
XssIgnoreRules ignoreRules = TL.get();
|
||||
if (ignoreRules == null) {
|
||||
return false;
|
||||
}
|
||||
String[] ignoreNames = ignoreRules.getNames();
|
||||
// 1. 如果没有设置忽略的字段
|
||||
if (ignoreNames.length == 0) {
|
||||
return true;
|
||||
}
|
||||
// 2. 指定忽略的属性
|
||||
return ObjectUtils.containsElement(ignoreNames, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为开启
|
||||
*/
|
||||
public static void setIgnore(XssIgnoreRules ignoreRules) {
|
||||
TL.set(ignoreRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 xss 清理
|
||||
*/
|
||||
public static void remove() {
|
||||
TL.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 忽略存储
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class XssIgnoreRules {
|
||||
|
||||
/**
|
||||
* 跳过的属性名
|
||||
*/
|
||||
private final String[] names;
|
||||
|
||||
public XssIgnoreRules() {
|
||||
this(new String[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.tool.utils.ClassUtil;
|
||||
import org.springblade.core.xss.annotation.XssIgnore;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.AsyncHandlerInterceptor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* xss 处理拦截器
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class XssInterceptor implements AsyncHandlerInterceptor {
|
||||
private final PathMatcher matcher = new AntPathMatcher();
|
||||
private final BladeXssProperties xssProperties;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
// 1. 非控制器请求直接跳出
|
||||
if (!(handler instanceof HandlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
// 2. 没有开启
|
||||
if (!xssProperties.isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
// 判断是否需要跳过
|
||||
List<String> skipUrl = xssProperties.getSkipUrl();
|
||||
String requestURL = request.getRequestURI();
|
||||
boolean needExclude = skipUrl.stream()
|
||||
.anyMatch(pattern -> matcher.match(pattern, requestURL));
|
||||
if (needExclude) {
|
||||
XssHolder.setIgnore(new XssIgnoreRules());
|
||||
return true;
|
||||
}
|
||||
// 3. 处理 XssIgnore 注解
|
||||
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||
XssIgnore xssIgnore = ClassUtil.getAnnotation(handlerMethod, XssIgnore.class);
|
||||
if (xssIgnore != null) {
|
||||
XssHolder.setIgnore(new XssIgnoreRules(xssIgnore.value()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
||||
XssHolder.remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
XssHolder.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss;
|
||||
|
||||
|
||||
import org.springblade.core.tool.utils.Exceptions;
|
||||
import org.springblade.core.xss.exception.FromXssException;
|
||||
import org.springblade.core.xss.exception.JacksonXssException;
|
||||
|
||||
/**
|
||||
* xss 数据处理类型
|
||||
*/
|
||||
public enum XssType {
|
||||
|
||||
/**
|
||||
* 表单
|
||||
*/
|
||||
FORM() {
|
||||
@Override
|
||||
public RuntimeException getXssException(String name, String input, String message) {
|
||||
return new FromXssException(input, message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* body json
|
||||
*/
|
||||
JACKSON() {
|
||||
@Override
|
||||
public RuntimeException getXssException(String name, String input, String message) {
|
||||
return Exceptions.unchecked(new JacksonXssException(name, input, message));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 xss 异常
|
||||
*
|
||||
* @param name 属性名
|
||||
* @param input input
|
||||
* @param message message
|
||||
* @return XssException
|
||||
*/
|
||||
public abstract RuntimeException getXssException(String name, String input, String message);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.core.xss;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.safety.Safelist;
|
||||
import org.springblade.core.tool.utils.StringPool;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
|
||||
/**
|
||||
* XSS工具类
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
public class XssUtil {
|
||||
|
||||
/**
|
||||
* 定义安全的HTML白名单
|
||||
*/
|
||||
public static final Safelist SAFE_LIST = Safelist.basicWithImages(); // 允许基本的HTML标签和图片
|
||||
|
||||
// 初始化白名单,可以根据需求添加或删除标签和属性
|
||||
static {
|
||||
// 添加额外的标签和属性
|
||||
SAFE_LIST.addTags("span", "div"); // 示例:添加额外的标签
|
||||
SAFE_LIST.addAttributes("div", "class"); // 示例:为特定标签添加安全属性
|
||||
}
|
||||
|
||||
/**
|
||||
* trim 字符串
|
||||
*
|
||||
* @param text text
|
||||
* @return 清理后的 text
|
||||
*/
|
||||
public static String trim(String text, boolean trim) {
|
||||
return trim ? text.trim() : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洁输入字符串,去除所有潜在的XSS攻击
|
||||
*
|
||||
* @param value 输入字符串
|
||||
* @return 清理后的字符串
|
||||
*/
|
||||
public static String clean(String value) {
|
||||
if (StringUtil.isNotBlank(value)) {
|
||||
return Jsoup.clean(value, SAFE_LIST);
|
||||
}
|
||||
return StringPool.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洁输入字符串,去除所有潜在的XSS攻击
|
||||
*
|
||||
* @param value 输入字符串
|
||||
* @param settings 输入设置
|
||||
* @return 清理后的字符串
|
||||
*/
|
||||
public static String clean(String value, Document.OutputSettings settings) {
|
||||
if (StringUtil.isNotBlank(value)) {
|
||||
return Jsoup.clean(value, StringPool.EMPTY, SAFE_LIST, settings);
|
||||
}
|
||||
return StringPool.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查输入字符串是否通过XSS检测
|
||||
*
|
||||
* @param value 输入字符串
|
||||
* @return 是否通过XSS检测
|
||||
*/
|
||||
public static boolean isPass(String value) {
|
||||
if (StringUtil.isBlank(value)) {
|
||||
return false;
|
||||
}
|
||||
// 清洁输入后得到的结果与输入字符串对比
|
||||
String cleanedString = Jsoup.clean(value, SAFE_LIST);
|
||||
return cleanedString.equals(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 忽略 xss
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface XssIgnore {
|
||||
|
||||
/**
|
||||
* 支持指定忽略的字段
|
||||
*
|
||||
* @return 字段数组
|
||||
*/
|
||||
String[] value() default {};
|
||||
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.xss.BladeXssProperties;
|
||||
import org.springblade.core.xss.XssInterceptor;
|
||||
import org.springblade.core.xss.processor.DefaultXssCleaner;
|
||||
import org.springblade.core.xss.processor.FormXssClean;
|
||||
import org.springblade.core.xss.processor.JacksonXssClean;
|
||||
import org.springblade.core.xss.processor.XssCleaner;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* jackson xss 配置
|
||||
*
|
||||
* @author L.cm,BladeX
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@RequiredArgsConstructor
|
||||
@EnableConfigurationProperties(BladeXssProperties.class)
|
||||
@ConditionalOnProperty(
|
||||
prefix = BladeXssProperties.PREFIX,
|
||||
name = "enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = true
|
||||
)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
public class BladeXssConfiguration implements WebMvcConfigurer {
|
||||
private final BladeXssProperties xssProperties;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public XssCleaner xssCleaner(BladeXssProperties properties) {
|
||||
return new DefaultXssCleaner(properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FormXssClean formXssClean(BladeXssProperties properties,
|
||||
XssCleaner xssCleaner) {
|
||||
return new FormXssClean(properties, xssCleaner);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(BladeXssProperties properties,
|
||||
XssCleaner xssCleaner) {
|
||||
return builder -> builder.deserializerByType(String.class, new JacksonXssClean(properties, xssCleaner));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(@Nonnull InterceptorRegistry registry) {
|
||||
XssInterceptor interceptor = new XssInterceptor(xssProperties);
|
||||
registry.addInterceptor(interceptor)
|
||||
.addPathPatterns(xssProperties.getBlockUrl())
|
||||
.addPathPatterns(xssProperties.getSkipUrl())
|
||||
.order(Ordered.LOWEST_PRECEDENCE);
|
||||
}
|
||||
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springblade.core.xss.exception.XssException;
|
||||
|
||||
/**
|
||||
* xss 表单异常
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Getter
|
||||
public class FromXssException extends IllegalStateException implements XssException {
|
||||
private final String input;
|
||||
|
||||
public FromXssException(String input, String message) {
|
||||
super(message);
|
||||
this.input = input;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springblade.core.xss.exception.XssException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* xss jackson 异常
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Getter
|
||||
public class JacksonXssException extends IOException implements XssException {
|
||||
private final String name;
|
||||
private final String input;
|
||||
|
||||
public JacksonXssException(String name, String input, String message) {
|
||||
super(message);
|
||||
this.name = name;
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss.exception;
|
||||
|
||||
/**
|
||||
* xss 异常,校验模式抛出
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
public interface XssException {
|
||||
|
||||
/**
|
||||
* 属性名,目前仅 body json 支持,form 表单不支持
|
||||
*
|
||||
* @return 属性名
|
||||
*/
|
||||
default String getName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入的数据
|
||||
*
|
||||
* @return 数据
|
||||
*/
|
||||
String getInput();
|
||||
|
||||
/**
|
||||
* 获取异常的消息
|
||||
*
|
||||
* @return 消息
|
||||
*/
|
||||
String getMessage();
|
||||
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss.processor;
|
||||
|
||||
import org.springblade.core.xss.BladeXssProperties;
|
||||
import org.springblade.core.xss.BladeXssProperties.Mode;
|
||||
import org.springblade.core.xss.XssType;
|
||||
import org.springblade.core.xss.XssUtil;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Entities;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 默认的 xss 清理器
|
||||
*
|
||||
* @author L.cm,BladeX
|
||||
*/
|
||||
public class DefaultXssCleaner implements XssCleaner {
|
||||
private final BladeXssProperties properties;
|
||||
|
||||
public DefaultXssCleaner(BladeXssProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
private static Document.OutputSettings getOutputSettings(BladeXssProperties properties) {
|
||||
return new Document.OutputSettings()
|
||||
// 转义
|
||||
.escapeMode(Entities.EscapeMode.xhtml)
|
||||
// 保留换行
|
||||
.prettyPrint(properties.isPrettyPrint());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String clean(String name, String bodyHtml, XssType type) {
|
||||
// 为空直接返回
|
||||
if (!StringUtils.hasText(bodyHtml)) {
|
||||
return bodyHtml;
|
||||
}
|
||||
Mode mode = properties.getMode();
|
||||
if (Mode.ESCAPE == mode) {
|
||||
// html 转义
|
||||
return HtmlUtils.htmlEscape(bodyHtml, StandardCharsets.UTF_8.name());
|
||||
} else if (Mode.VALIDATE == mode) {
|
||||
// 校验
|
||||
if (Jsoup.isValid(bodyHtml, XssUtil.SAFE_LIST)) {
|
||||
return bodyHtml;
|
||||
}
|
||||
throw type.getXssException(name, bodyHtml, "Xss validate fail, input value:" + bodyHtml);
|
||||
} else {
|
||||
if (properties.isEnableEscape()) {
|
||||
// 转义
|
||||
bodyHtml = Entities.escape(bodyHtml);
|
||||
} else {
|
||||
// 反转义
|
||||
bodyHtml = Entities.unescape(bodyHtml);
|
||||
}
|
||||
return XssUtil.clean(bodyHtml, getOutputSettings(properties));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss.processor;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.auto.annotation.AutoIgnore;
|
||||
import org.springblade.core.xss.BladeXssProperties;
|
||||
import org.springblade.core.xss.XssHolder;
|
||||
import org.springblade.core.xss.XssType;
|
||||
import org.springblade.core.xss.XssUtil;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
|
||||
import java.beans.PropertyEditorSupport;
|
||||
|
||||
/**
|
||||
* 表单 xss 处理
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@AutoIgnore
|
||||
@ControllerAdvice
|
||||
@ConditionalOnProperty(
|
||||
prefix = BladeXssProperties.PREFIX,
|
||||
name = "enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = true
|
||||
)
|
||||
@RequiredArgsConstructor
|
||||
public class FormXssClean {
|
||||
private final BladeXssProperties properties;
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
@InitBinder
|
||||
public void initBinder(WebDataBinder binder) {
|
||||
// 处理前端传来的表单字符串
|
||||
binder.registerCustomEditor(String.class, new StringPropertiesEditor(xssCleaner, properties));
|
||||
}
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public static class StringPropertiesEditor extends PropertyEditorSupport {
|
||||
private final XssCleaner xssCleaner;
|
||||
private final BladeXssProperties properties;
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
if (text == null) {
|
||||
setValue(null);
|
||||
} else if (XssHolder.isEnabled()) {
|
||||
String value = xssCleaner.clean(XssUtil.trim(text, properties.isTrimText()), XssType.FORM);
|
||||
setValue(value);
|
||||
log.debug("Request parameter value:{} cleaned up by blade-xss, current value is:{}.", text, value);
|
||||
} else {
|
||||
setValue(XssUtil.trim(text, properties.isTrimText()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss.processor;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.xss.BladeXssProperties;
|
||||
import org.springblade.core.xss.XssHolder;
|
||||
import org.springblade.core.xss.XssType;
|
||||
import org.springblade.core.xss.XssUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* jackson xss 处理
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class JacksonXssClean extends XssDeserializerBase {
|
||||
private final BladeXssProperties properties;
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
@Override
|
||||
public String clean(String name, String text) throws IOException {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
// 判断是否忽略
|
||||
if (XssHolder.isIgnore(name)) {
|
||||
return XssUtil.trim(text, properties.isTrimText());
|
||||
}
|
||||
String value = xssCleaner.clean(name, XssUtil.trim(text, properties.isTrimText()), XssType.JACKSON);
|
||||
log.debug("Json property name:{} value:{} cleaned up by blade-xss, current value is:{}.", name, text, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss.processor;
|
||||
|
||||
import org.springblade.core.xss.XssType;
|
||||
import org.springblade.core.xss.XssUtil;
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
/**
|
||||
* xss 清理器
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
public interface XssCleaner {
|
||||
|
||||
/**
|
||||
* 清理 html
|
||||
*
|
||||
* @param value 属性值
|
||||
* @param type XssType
|
||||
* @return 清理后的数据
|
||||
*/
|
||||
default String clean(String value, XssType type) {
|
||||
return clean(null, value, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理 html
|
||||
*
|
||||
* @param name 属性名
|
||||
* @param value 属性值
|
||||
* @param type XssType
|
||||
* @return 清理后的数据
|
||||
*/
|
||||
String clean(String name, String value, XssType type);
|
||||
|
||||
/**
|
||||
* 判断输入是否安全
|
||||
*
|
||||
* @param html html
|
||||
* @return 是否安全
|
||||
*/
|
||||
default boolean isValid(String html) {
|
||||
return Jsoup.isValid(html, XssUtil.SAFE_LIST);
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss.processor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
import org.springblade.core.xss.BladeXssProperties;
|
||||
import org.springblade.core.xss.XssType;
|
||||
import org.springblade.core.xss.XssUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* jackson xss 处理
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
@Slf4j
|
||||
public class XssDeserializer extends XssDeserializerBase {
|
||||
|
||||
@Override
|
||||
public String clean(String name, String text) throws IOException {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
// 读取 xss 配置
|
||||
BladeXssProperties properties = SpringUtil.getBean(BladeXssProperties.class);
|
||||
if (Func.isEmpty(properties)) {
|
||||
return text;
|
||||
}
|
||||
// 读取 XssCleaner bean
|
||||
XssCleaner xssCleaner = SpringUtil.getBean(XssCleaner.class);
|
||||
if (Func.isEmpty(properties)) {
|
||||
return XssUtil.trim(text, properties.isTrimText());
|
||||
}
|
||||
String value = xssCleaner.clean(name, XssUtil.trim(text, properties.isTrimText()), XssType.JACKSON);
|
||||
log.debug("Json property name:{} value:{} cleaned up by blade-xss, current value is:{}.", name, text, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: DreamLu (596392912@qq.com)
|
||||
*/
|
||||
|
||||
package org.springblade.core.xss.processor;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* jackson xss 处理
|
||||
*
|
||||
* @author L.cm
|
||||
*/
|
||||
public abstract class XssDeserializerBase extends JsonDeserializer<String> {
|
||||
|
||||
@Override
|
||||
public String deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
|
||||
// json 字段名
|
||||
String name = p.currentName();
|
||||
// 字符串类型
|
||||
if (p.hasToken(JsonToken.VALUE_STRING)) {
|
||||
String text = p.getText();
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
return clean(name, text);
|
||||
}
|
||||
JsonToken jsonToken = p.getCurrentToken();
|
||||
if (jsonToken.isScalarValue()) {
|
||||
String text = p.getValueAsString();
|
||||
if (text != null) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
throw MismatchedInputException.from(p, String.class, "blade-xss: can't deserialize json name:" + name + " value of type java.lang.String from " + jsonToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理 xss
|
||||
*
|
||||
* @param name json name
|
||||
* @param value json value
|
||||
* @return String
|
||||
*/
|
||||
public abstract String clean(String name, String value) throws IOException;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user