在Spring Boot中,我们可以使用注解的方式来进行XSS防御。注解是一种轻量级的防御手段,它可以在方法或字段级别对输入进行校验,从而防止XSS攻击。
而想对全局的请求都进行XSS防御可以使用servlet中的过滤器或者spring mvc中的拦截器,这里使用servlet中的过滤器进行演示。
maven依赖:
1 2 3 4 5 6 |
<!--JSR-303/JSR-380用于验证的注解 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>2.6.7</version> </dependency> |
如果是使用grade,引入依赖:
1 |
implementation 'org.springframework.boot:spring-boot-starter-validation:2.6.7' |
1 2 3 4 |
xss: enabled: true excludeUrlList: - /xss/local/test |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.morris.spring.boot.module.xss;
import lombok.Data;
import java.util.List;
@Data public class XssFilterProperties { /** * 是否启用XSS过滤。 */ private boolean enabled = true;
/** * 需要排除的URL模式,这些URL不会进行XSS过滤。 */ private List<String> excludeUrlList; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package com.morris.spring.boot.module.xss;
import lombok.Data; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter;
import javax.servlet.DispatcherType;
@Data @Configuration public class XssFilterConfig {
@ConfigurationProperties(prefix = "xss") @Bean public XssFilterProperties xssFilterProperties() { return new XssFilterProperties(); }
/** * 注册XSS过滤器。 * * @return FilterRegistrationBean 用于注册过滤器的bean。 */ @Bean public FilterRegistrationBean<XssFilter> xssFilterRegistration(XssFilterProperties xssFilterProperties) { FilterRegistrationBean<XssFilter> registrationBean = new FilterRegistrationBean<>(); // 设置过滤器的分发类型为请求类型 registrationBean.setDispatcherTypes(DispatcherType.REQUEST); // 创建XssFilter的实例 registrationBean.setFilter(new XssFilter(xssFilterProperties)); // 添加过滤器需要拦截的URL模式,这里拦截所有请求 registrationBean.addUrlPatterns("/*"); // 设置过滤器的名称 registrationBean.setName("XssFilter"); // 设置过滤器的执行顺序,数值越小,优先级越高 registrationBean.setOrder(9999); return registrationBean; }
@Bean public HttpMessageConverters xssHttpMessageConverters() { XSSMappingJackson2HttpMessageConverter xssMappingJackson2HttpMessageConverter = new XSSMappingJackson2HttpMessageConverter(); HttpMessageConverter converter = xssMappingJackson2HttpMessageConverter; return new HttpMessageConverters(converter); }
} |
XssFilter过滤器会将所有需要进行防御的请求包装为XssWrapper。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
package com.morris.spring.boot.module.xss;
import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils;
import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern;
@Slf4j public class XssFilter implements Filter {
private final XssFilterProperties xssFilterProperties;
public XssFilter(XssFilterProperties xssFilterProperties) { this.xssFilterProperties = xssFilterProperties; }
/** * 执行过滤逻辑,如果当前请求不在排除列表中,则通过XSS过滤器包装请求。 * * @param request HTTP请求对象。 * @param response HTTP响应对象。 * @param chain 过滤器链对象,用于继续或中断请求处理。 * @throws IOException 如果处理过程中出现I/O错误。 * @throws ServletException 如果处理过程中出现Servlet相关错误。 */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; //如果该访问接口在排除列表里面则不拦截 if (isExcludeUrl(req.getServletPath())) { chain.doFilter(request, response); return; }
log.info("uri:{}", req.getRequestURI()); // xss 过滤 chain.doFilter(new XssWrapper(req), resp); }
/** * 判断当前请求的URL是否应该被排除在XSS过滤之外。 * * @param urlPath 请求的URL路径。 * @return 如果请求应该被排除,则返回true;否则返回false。 */ private boolean isExcludeUrl(String urlPath) { if (!xssFilterProperties.isEnabled()) { //如果xss开关关闭了,则所有url都不拦截 return true; }
if(CollectionUtils.isEmpty(xssFilterProperties.getExcludeUrlList())) { return false; }
for (String pattern : xssFilterProperties.getExcludeUrlList()) { Pattern p = Pattern.compile("^" + pattern); Matcher m = p.matcher(urlPath); if (m.find()) { return true; } } return false; } } |
XssWrapper会过滤get请求和请求头中的非法字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
package com.morris.spring.boot.module.xss;
import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper;
@Slf4j public class XssWrapper extends HttpServletRequestWrapper {
public XssWrapper(HttpServletRequest request) { super(request); }
/** * 对数组参数进行特殊字符过滤 */ @Override public String[] getParameterValues(String name) { String[] values = super.getParameterValues(name); if (values == null) { return null; } int count = values.length; String[] encodedValues = new String[count]; for (int i = 0; i < count; i++) { encodedValues[i] = XssUtil.clean(values[i]); } return encodedValues; }
/** * 对参数中特殊字符进行过滤 */ @Override public String getParameter(String name) { String value = super.getParameter(name); if (StringUtils.isBlank(value)) { return value; } return XssUtil.clean(value); }
/** * 获取attribute,特殊字符过滤 */ @Override public Object getAttribute(String name) { Object value = super.getAttribute(name); if (value instanceof String && StringUtils.isNotBlank((String) value)) { return XssUtil.clean((String) value); } return value; }
/** * 对请求头部进行特殊字符过滤 */ @Override public String getHeader(String name) { String value = super.getHeader(name); if (StringUtils.isBlank(value)) { return value; } return XssUtil.clean(value); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
package com.morris.spring.boot.module.xss;
import com.fasterxml.jackson.databind.JavaType; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import java.io.IOException; import java.lang.reflect.Type;
/** * 在读取和写入JSON数据时特殊字符避免xss攻击的消息解析器 * */ public class XSSMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
/** * 从HTTP输入消息中读取对象,同时应用XSS防护。 * * @param type 类型令牌,表示要读取的对象类型。 * @param contextClass 上下文类,提供类型解析的上下文信息。 * @param inputMessage HTTP输入消息,包含要读取的JSON数据。 * @return 从输入消息中解析出的对象,经过XSS防护处理。 * @throws IOException 如果发生I/O错误。 * @throws HttpMessageNotReadableException 如果消息无法读取。 */ @Override public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { JavaType javaType = getJavaType(type, contextClass); Object obj = readJavaType(javaType, inputMessage); //得到请求json String json = super.getObjectMapper().writeValueAsString(obj); //过滤特殊字符 String result = XssUtil.clean(json); Object resultObj = super.getObjectMapper().readValue(result, javaType); return resultObj; }
/** * 从HTTP输入消息中读取指定Java类型的对象,内部使用。 * * @param javaType 要读取的对象的Java类型。 * @param inputMessage HTTP输入消息,包含要读取的JSON数据。 * @return 从输入消息中解析出的对象。 * @throws IOException 如果发生I/O错误。 * @throws HttpMessageNotReadableException 如果消息无法读取。 */ private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) { try { return super.getObjectMapper().readValue(inputMessage.getBody(), javaType); } catch (IOException ex) { throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex); } }
/** * 将对象写入HTTP输出消息,同时应用XSS防护。 * * @param object 要写入的对象。 * @param outputMessage HTTP输出消息,对象将被序列化为JSON并写入此消息。 * @throws IOException 如果发生I/O错误。 * @throws HttpMessageNotWritableException 如果消息无法写入。 */ @Override protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { //得到要输出的json String json = super.getObjectMapper().writeValueAsString(object); //过滤特殊字符 String result = XssUtil.clean(json); // 输出 outputMessage.getBody().write(result.getBytes()); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
package com.morris.spring.boot.module.xss;
import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.safety.Whitelist;
/** * XSS过滤工具类,使用Jsoup库对输入的字符串进行XSS攻击防护 */ public class XssUtil {
/** * 使用jsoup自带的relaxed白名单 */ private static final Whitelist WHITE_LIST = Whitelist.relaxed(); /** * 定义输出设置,关闭prettyPrint(prettyPrint=false),目的是避免在清理过程中对代码进行格式化 * 从而保持输入和输出内容的一致性。 */ private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
/* 初始化白名单策略,允许所有标签拥有style属性。 这是因为在富文本编辑中,样式通常通过style属性来定义,需要确保这些样式能够被保留。 */ static { // 富文本编辑时一些样式是使用 style 来进行实现的 // 比如红色字体 style="color:red;" // 所以需要给所有标签添加 style 属性 WHITE_LIST.addAttributes(":all", "style"); }
/** * 清理输入的字符串,移除潜在的XSS攻击代码。 * * @param content 待清理的字符串,通常是用户输入的HTML内容。 * @return 清理后的字符串,保证不包含XSS攻击代码。 */ public static String clean(String content) { // 使用定义好的白名单策略和输出设置清理输入的字符串 return Jsoup.clean(content, "", WHITE_LIST, OUTPUT_SETTINGS); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package com.morris.spring.boot.module.xss;
import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
/** * Xss2防御get请求 */ @RestController @RequestMapping("/xss/global") @Validated public class XssGlobalGetController {
/** * 使用注解拦截get请求中的xss,在方法参数前面加上@Xss,注意类上面要加上@Validated注解 * * @param userAccount 请求参数 * @return 请求参数 */ @GetMapping("/test") public String test(String userAccount) { return userAccount; }
} |
发送get请求:http://localhost:8888/xss/global/test?userAccount=demoData
返回结果:demoData
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package com.morris.spring.boot.module.xss;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
/** * Xss全局防御post请求 */ @RestController @RequestMapping("/xss/global") public class XssGlobalPostController {
/** * 使用注解拦截POST请求中的xss,在实体类需要拦截xss的属性上面加上@Xss或者@Validated注解 * * @param userGlobalLoginPojo 实体类 * @return 实体类 */ @PostMapping("/test") public UserGlobalLoginPojo test(@RequestBody UserGlobalLoginPojo userGlobalLoginPojo) { return userGlobalLoginPojo; }
} |
发送post请求:http://localhost:8888/xss/global/test
请求体:
{
"userAccount": "<iframe οnlοad='alert(0)'>demoData</iframe>"
}
返回结果:
{
"userAccount": "demoData"
}