一、引言
什么是XSS攻击?
跨站脚本攻击(Cross-Site Scripting,简称XSS)是一种常见的Web安全漏洞,它允许攻击者将恶意脚本注入到其他用户浏览和使用的正常网页中。当其他用户浏览这些网页时,恶意脚本就会在他们的浏览器上执行,从而可能导致信息泄露、会话劫持等严重后果。XSS攻击的普遍性和潜在危害性使其成为Web应用安全中不可忽视的一部分。
XSS攻击的定义
XSS攻击是指攻击者在Web页面的输入数据中插入恶意脚本,当其他用户浏览该页面时,这些脚本就会在用户的浏览器上执行。由于脚本是在受害用户的上下文中执行的,因此它可以访问该用户的所有会话信息和权限,从而可能导致信息泄露、会话劫持、恶意操作等安全风险。
- 存储型XSS(Persistent XSS):恶意脚本被永久存储在目标服务器上,如数据库、消息论坛、访客留言等,当用户访问相应的网页时,恶意脚本就会执行。
- 反射型XSS(Reflected XSS):恶意脚本并不存储在目标服务器上,而是通过诸如URL参数的方式直接在请求响应中反射并执行。这种类型的攻击通常是通过诱使用户点击链接或访问特定的URL来实施的。
- 基于DOM的XSS(DOM-based XSS):这种类型的XSS攻击完全发生在客户端,不需要服务器的参与。它通过恶意脚本修改页面的DOM结构,实现攻击。
XSS攻击方式
-
盗用cookie,获取敏感信息。
-
利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。
-
利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。
-
利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
-
在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果。
二、解决方案
引入依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
创建配置文件
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zzx.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
/**
* Xss配置类
*
* @author zzx
*/
@Getter
@Setter
@ConfigurationProperties(ZzxXssProperties .PREFIX)
public class ZzxXssProperties {
public static final String PREFIX = "security.xss";
/**
* 开启xss,默认关闭: 建议生产环境开启
*/
private boolean enabled = false;
/**
* 全局:对文件进行首尾 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> pathPatterns = new ArrayList<>();
/**
* 放行的路由,默认为空
*/
private List<String> pathExcludePatterns = new ArrayList<>();
public enum Mode {
/**
* 清理
*/
clear,
/**
* 转义
*/
escape;
}
}
增加XSS处理工具
/*
* Copyright (c) 2016-2020, Michael Yang 杨福海 (fuhai999@gmail.com).
* <p>
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl-3.0.txt
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zzx.utils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Attribute;
import org.jsoup.nodes.Element;
import org.springframework.util.StringUtils;
/**
* xss clean
*
* <p>
* 参考自 jpress:https://gitee.com/fuhai/jpress
* </p>
*
* @author zzx
*/
public class XssUtil {
public static final HtmlWhitelist WHITE_LIST = new HtmlWhitelist();
/**
* trim 字符串
* @param text text
* @return 清理后的 text
*/
public static String trim(String text, boolean trim) {
return trim ? StringUtils.trimWhitespace(text) : text;
}
/**
* xss 清理
* @param html html
* @return 清理后的 html
*/
public static String clean(String html) {
if (StringUtils.hasText(html)) {
return Jsoup.clean(html, WHITE_LIST);
}
return html;
}
/**
* 做自己的白名单,允许base64的图片通过等
*
* @author michael
*/
public static class HtmlWhitelist extends org.jsoup.safety.Whitelist {
public HtmlWhitelist() {
addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "span",
"embed", "object", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "img", "li", "ol",
"p", "pre", "q", "small", "strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th",
"thead", "tr", "u", "ul");
addAttributes("a", "href", "title", "target");
addAttributes("blockquote", "cite");
addAttributes("col", "span");
addAttributes("colgroup", "span");
addAttributes("img", "align", "alt", "src", "title");
addAttributes("ol", "start");
addAttributes("q", "cite");
addAttributes("table", "summary");
addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width");
addAttributes("th", "abbr", "axis", "colspan", "rowspan", "scope", "width");
addAttributes("video", "src", "autoplay", "controls", "loop", "muted", "poster", "preload");
addAttributes("object", "width", "height", "classid", "codebase");
addAttributes("param", "name", "value");
addAttributes("embed", "src", "quality", "width", "height", "allowFullScreen", "allowScriptAccess",
"flashvars", "name", "type", "pluginspage");
addAttributes(":all", "class", "style", "height", "width", "type", "id", "name");
addProtocols("blockquote", "cite", "http", "https");
addProtocols("cite", "cite", "http", "https");
addProtocols("q", "cite", "http", "https");
// 如果添加以下的协议,那么href 必须是http、 https 等开头,相对路径则被过滤掉了
// addProtocols("a", "href", "ftp", "http", "https", "mailto", "tel");
// 如果添加以下的协议,那么src必须是http 或者 https 开头,相对路径则被过滤掉了,
// 所以必须注释掉,允许相对路径的图片资源
// addProtocols("img", "src", "http", "https");
}
@Override
protected boolean isSafeAttribute(String tagName, Element el, Attribute attr) {
// 不允许 javascript 开头的 src 和 href
if ("src".equalsIgnoreCase(attr.getKey()) || "href".equalsIgnoreCase(attr.getKey())) {
String value = attr.getValue();
if (StringUtils.hasText(value) && value.toLowerCase().startsWith("javascript")) {
return false;
}
}
// 允许 base64 的图片内容
if ("img".equals(tagName) && "src".equals(attr.getKey()) && attr.getValue().startsWith("data:;base64")) {
return true;
}
return super.isSafeAttribute(tagName, el, attr);
}
}
}
xss 清理器
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zzx.core;
/**
* xss 清理器
*
* @author zzx
*/
public interface XssCleaner {
/**
* 清理 html
* @param html html
* @return 清理后的数据
*/
String clean(String html);
}
默认的 xss 清理器
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zzx.core;
import cn.hutool.core.util.CharsetUtil;
import com.zzx.ZzxXssProperties;
import com.zzx.XssUtil;
import lombok.RequiredArgsConstructor;
import org.jsoup.Jsoup;
import org.jsoup.internal.StringUtil;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Entities;
import org.jsoup.safety.Cleaner;
import org.springframework.web.util.HtmlUtils;
/**
* 默认的 xss 清理器
*
* @author zzx
*/
@RequiredArgsConstructor
public class DefaultXssCleaner implements XssCleaner {
private final ZzxXssProperties properties;
@Override
public String clean(String bodyHtml) {
// 1. 为空直接返回
if (StringUtil.isBlank(bodyHtml)) {
return bodyHtml;
}
ZzxXssProperties.Mode mode = properties.getMode();
if (ZzxXssProperties.Mode.escape == mode) {
// html 转义
return HtmlUtils.htmlEscape(bodyHtml, CharsetUtil.UTF_8);
}
else {
// jsoup html 清理
Document.OutputSettings outputSettings = new Document.OutputSettings()
// 2. 转义,没找到关闭的方法,目前这个规则最少
.escapeMode(Entities.EscapeMode.xhtml)
// 3. 保留换行
.prettyPrint(properties.isPrettyPrint());
Document dirty = Jsoup.parseBodyFragment(bodyHtml, "");
Cleaner cleaner = new Cleaner(XssUtil.WHITE_LIST);
Document clean = cleaner.clean(dirty);
clean.outputSettings(outputSettings);
// 4. 清理后的 html
String escapedHtml = clean.body().html();
if (properties.isEnableEscape()) {
return escapedHtml;
}
// 5. 反转义
return Entities.unescape(escapedHtml);
}
}
}
增加XSS-Form表单内容XSS
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zzx.core;
import cn.hutool.core.util.StrUtil;
import com.zzx.ZzxXssProperties;
import com.zzx.XssUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 zzx
*/
@ControllerAdvice
@RequiredArgsConstructor
public class FormXssClean {
private final ZzxXssProperties 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 ZzxXssProperties properties;
@Override
public String getAsText() {
Object value = getValue();
return value != null ? value.toString() : StrUtil.EMPTY;
}
@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()));
setValue(value);
log.debug("Request parameter value:{} cleaned up by mica-xss, current value is:{}.", text, value);
}
else {
setValue(XssUtil.trim(text, properties.isTrimText()));
}
}
}
}
jackson xss 处理
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zzx.core;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.zzx.ZzxXssProperties;
import com.zzx.XssUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* jackson xss 处理
*
* @author zzx
*/
@Slf4j
@RequiredArgsConstructor
public class JacksonXssClean extends JsonDeserializer<String> {
private final ZzxXssProperties properties;
private final XssCleaner xssCleaner;
@Override
public String deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
// XSS filter
String text = p.getValueAsString();
if (text == null) {
return null;
}
if (XssHolder.isEnabled()) {
String value = xssCleaner.clean(XssUtil.trim(text, properties.isTrimText()));
log.debug("Json property value:{} cleaned up by mica-xss, current value is:{}.", text, value);
return value;
}
else {
return XssUtil.trim(text, properties.isTrimText());
}
}
}
增加XSS配置类
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zzx.xss;
import com.zzx.ZzxXssProperties;
import com.zzx.*;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* jackson xss 配置
*
* @author zzx
*/
@RequiredArgsConstructor
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ZzxXssProperties.class)
@ConditionalOnProperty(prefix = ZzxXssProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class ZzxXssAutoConfiguration implements WebMvcConfigurer {
private final ZzxXssPropertiesxssProperties;
@Bean
@ConditionalOnMissingBean
public XssCleaner xssCleaner(ZzxXssProperties properties) {
return new DefaultXssCleaner(properties);
}
@Bean
@ConditionalOnMissingBean
public FormXssClean formXssClean(ZzxXssProperties properties, XssCleaner xssCleaner) {
return new FormXssClean(properties, xssCleaner);
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(ZzxXssProperties properties,
XssCleaner xssCleaner) {
JacksonXssClean xssClean = new JacksonXssClean(properties, xssCleaner);
return builder -> builder.deserializerByType(String.class, xssClean);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> patterns = xssProperties.getPathPatterns();
if (patterns.isEmpty()) {
patterns.add("/**");
}
XssCleanInterceptor interceptor = new XssCleanInterceptor(xssProperties);
registry.addInterceptor(interceptor)
.addPathPatterns(patterns)
.excludePathPatterns(xssProperties.getPathExcludePatterns())
.order(Ordered.LOWEST_PRECEDENCE);
}
}
忽略 xss 注解
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zzx.core;
import java.lang.annotation.*;
/**
* 忽略 xss
*
* @author zzx
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface XssCleanIgnore {
}
xss 处理拦截器
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.zzx.core;
import com.zzx.ZzxXssProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* xss 处理拦截器
*
* @author zzx
*/
@RequiredArgsConstructor
public class XssCleanInterceptor implements AsyncHandlerInterceptor {
private final ZzxXssProperties 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;
}
// 3. 处理 XssIgnore 注解
HandlerMethod handlerMethod = (HandlerMethod) handler;
XssCleanIgnore xssCleanIgnore = AnnotationUtils.getAnnotation(handlerMethod.getMethod(), XssCleanIgnore.class);
if (xssCleanIgnore == null) {
XssHolder.setEnable();
}
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();
}
}
xss clean 工具
/*
* Copyright (c) 2016-2020, Michael Yang 杨福海 (fuhai999@gmail.com).
* <p>
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl-3.0.txt
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yasee.cdms.common.xss.utils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Attribute;
import org.jsoup.nodes.Element;
import org.springframework.util.StringUtils;
/**
* xss clean
*
* <p>
* 参考自 jpress:https://gitee.com/fuhai/jpress
* </p>
*
* @author L.cm
* @author michael
*/
public class XssUtil {
public static final HtmlWhitelist WHITE_LIST = new HtmlWhitelist();
/**
* trim 字符串
* @param text text
* @return 清理后的 text
*/
public static String trim(String text, boolean trim) {
return trim ? StringUtils.trimWhitespace(text) : text;
}
/**
* xss 清理
* @param html html
* @return 清理后的 html
*/
public static String clean(String html) {
if (StringUtils.hasText(html)) {
return Jsoup.clean(html, WHITE_LIST);
}
return html;
}
/**
* 做自己的白名单,允许base64的图片通过等
*
* @author michael
*/
public static class HtmlWhitelist extends org.jsoup.safety.Whitelist {
public HtmlWhitelist() {
addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "span",
"embed", "object", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "img", "li", "ol",
"p", "pre", "q", "small", "strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th",
"thead", "tr", "u", "ul");
addAttributes("a", "href", "title", "target");
addAttributes("blockquote", "cite");
addAttributes("col", "span");
addAttributes("colgroup", "span");
addAttributes("img", "align", "alt", "src", "title");
addAttributes("ol", "start");
addAttributes("q", "cite");
addAttributes("table", "summary");
addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width");
addAttributes("th", "abbr", "axis", "colspan", "rowspan", "scope", "width");
addAttributes("video", "src", "autoplay", "controls", "loop", "muted", "poster", "preload");
addAttributes("object", "width", "height", "classid", "codebase");
addAttributes("param", "name", "value");
addAttributes("embed", "src", "quality", "width", "height", "allowFullScreen", "allowScriptAccess",
"flashvars", "name", "type", "pluginspage");
addAttributes(":all", "class", "style", "height", "width", "type", "id", "name");
addProtocols("blockquote", "cite", "http", "https");
addProtocols("cite", "cite", "http", "https");
addProtocols("q", "cite", "http", "https");
// 如果添加以下的协议,那么href 必须是http、 https 等开头,相对路径则被过滤掉了
// addProtocols("a", "href", "ftp", "http", "https", "mailto", "tel");
// 如果添加以下的协议,那么src必须是http 或者 https 开头,相对路径则被过滤掉了,
// 所以必须注释掉,允许相对路径的图片资源
// addProtocols("img", "src", "http", "https");
}
@Override
protected boolean isSafeAttribute(String tagName, Element el, Attribute attr) {
// 不允许 javascript 开头的 src 和 href
if ("src".equalsIgnoreCase(attr.getKey()) || "href".equalsIgnoreCase(attr.getKey())) {
String value = attr.getValue();
if (StringUtils.hasText(value) && value.toLowerCase().startsWith("javascript")) {
return false;
}
}
// 允许 base64 的图片内容
if ("img".equals(tagName) && "src".equals(attr.getKey()) && attr.getValue().startsWith("data:;base64")) {
return true;
}
return super.isSafeAttribute(tagName, el, attr);
}
}
}