您的位置:

Springboot XSS过滤详解

XSS(Cross-site scripting)跨站脚本攻击是一种常见的Web攻击,攻击者往Web页面里插入恶意的脚本代码,使得用户在浏览页面或提交表单的时候,执行恶意脚本,从而达到攻击目的。为了防止XSS攻击,Springboot提供了多种方式去过滤用户输入的数据,保证系统的安全稳定。

一、 Springboot过滤器

1、过滤器介绍 过滤器是用于拦截请求与响应,可以用于URL、Servlet、JSP、静态文件等资源的拦截。Springboot的过滤器实现了javax.servlet.Filter接口,提供了过滤器的常见功能,是一种非常常用的过滤方式。 2、过滤器示例 在Springboot中,过滤器是通过@Bean把过滤器加入到过滤器链中的,可以通过注解@WebFilter实现。以下示例会构建一个过滤器,对所有请求进行XSS过滤:
@WebFilter(urlPatterns = "/*", filterName = "xssFilter")
public class XssFilter implements Filter {
  @Override
  public void init(FilterConfig filterConfig) throws ServletException { }
  
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response);
  }
  
  @Override
  public void destroy() { }
}
在过滤器中,我们通过构建XssHttpServletRequestWrapper来过滤请求。这个Wrapper里重写了getParameter方法,对请求中的参数进行了过滤。XssHttpServletRequestWrapper的实现如下:
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
  public XssHttpServletRequestWrapper(HttpServletRequest request) {
    super(request);
  }

  @Override
  public String getParameter(String name) {
    String value = super.getParameter(name);
    if (value == null) {
        return null;
    }
    return HtmlUtils.htmlEscape(value);
  }
}
在getParameter方法中,我们使用了org.springframework.web.util.HtmlUtils的htmlEscape方法对请求参数进行了过滤,把所有的HTML标签都转换成了实体,防止了XSS攻击。

二、 Springboot注解

1、注解介绍 Springboot通过注解的方式来处理XSS过滤,可以直接在Controller层的方法参数上使用@Validated注解,在参数中使用@NotBlank或其他JSR303注解,并配合@RequestBody、@RequestParam可以过滤掉所有HTML标签,非常方便易用。 2、注解示例 以下例子演示了如何使用@RequestBody和@RequestParam结合JSR303注解来实现对控制器请求参数的过滤:
@RestController
public class UserController {
    @PostMapping("/user")
    public void addUser(@Validated @RequestBody UserDTO userDTO) {
        // Todo
    }
    
    @GetMapping("/users")
    public List getUsers(@RequestParam @NotBlank String name) {
        // Todo
    }
}

  
在addUser方法中,我们使用了@Validated注解,同时在UserDTO的name字段上使用了@NotBlank注解,来实现对请求参数的过滤。在getUsers方法中,我们也显示的用@RequestParam声明了参数name,并使用了@NotBlank注解来过滤请求。

三、Springboot过滤器加上自定义注解

1、自定义注解介绍 自定义注解可以让我们更好的管理XSS过滤,使得代码更加可读、易用、易维护,可以在Springboot过滤器的基础上添加自定义注解,来实现更加灵活的过滤。 2、自定义注解示例 以下代码演示了如何在过滤器中添加自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface XssSecurity {
     String[] excludes() default {};
}

@Aspect
@Component
public class XssSecurityAspect {
  private static final Logger logger = LoggerFactory.getLogger(XssSecurityAspect.class);

  @Pointcut("@annotation(cn.example.demo.annotation.XssSecurity)")
  public void xssPointCut() {
  }

  @Before("xssPointCut() && @annotation(xssSecurity)")
  public void doBefore(JoinPoint joinPoint, XssSecurity xssSecurity) throws Throwable {
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if (servletRequestAttributes == null) {
        return;
    }
    HttpServletRequest request = servletRequestAttributes.getRequest();
    if (checkUrlExclude(request.getRequestURI(), xssSecurity.excludes())) {
        return;
    }
    XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(request);
    request.setAttribute("body", xssRequest.getBody());
    for (Object arg : joinPoint.getArgs()) {
        if (arg instanceof XssHttpServletRequestWrapper || arg instanceof MultipartFile) {
            continue;
        }
        Object o = xssSecurity(xssRequest, arg);
        Method method = reflectMethod(joinPoint, arg);
        if (null != method) {
            method.invoke(joinPoint.getTarget(), o);
        }
    }
  }

  public Object xssSecurity(HttpServletRequest request, Object param) {
    if (param == null) {
        return null;
    }
    if (param instanceof String) {
        return HtmlUtils.htmlEscape((String) param);
    } else if (param instanceof Map) {
        Map paramMap = new HashMap<>();
        Map objectMap = (Map) param;
        for (Map.Entry entry : objectMap.entrySet()) {
            paramMap.put(entry.getKey(), (!isSkip(entry.getKey().toString())) ? xssSecurity(request, entry.getValue()) : entry.getValue());
        }
        return paramMap;
    } else if (param instanceof Object[]) {
        Object[] paramObj = (Object[]) param;
        for (Object object : paramObj) {
            if (object instanceof XssHttpServletRequestWrapper || object instanceof MultipartFile) {
                continue;
            }
            xssSecurity(request, object);
        }
        return paramObj;
    } else if (param instanceof Collection) {
        Collection objs = (Collection) param;
        for (Object obj : objs) {
            if (obj instanceof XssHttpServletRequestWrapper || obj instanceof MultipartFile) {
                continue;
            }
            xssSecurity(request, obj);
        }
        return objs;
    } else if (param.getClass().isArray()) {
        int len = Array.getLength(param);
        for (int i = 0; i < len; ++i) {
            Object object = Array.get(param, i);
            if (object instanceof XssHttpServletRequestWrapper || object instanceof MultipartFile) {
                continue;
            }
            xssSecurity(request, object);
        }
        return param;
    }
    return param;
  }

  private boolean isSkip(String key) {
    boolean flag = false;
    String param = null;
    if (StringUtils.isNotEmpty(key)) {
        param = key.toLowerCase();
    }
    for (String exclude : XSSConstant.exclude) {
        if (exclude.equals(param)) {
            flag = true;
            break;
        }
    }
    return flag;
  }
  
  private Method reflectMethod(JoinPoint joinPoint, Object arg) {
    Method m = null;
    Method[] methods = joinPoint.getTarget().getClass().getDeclaredMethods();
    String methodName = joinPoint.getSignature().getName();
    if (null != arg) {
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                if (method.getParameterTypes() != null && method.getParameterTypes().length == 1
                    && method.getParameterTypes()[0].isAssignableFrom(arg.getClass())) {
                    m = method;
                    break;
                }
            }
        }
    }
    return m;
  }
}

@XssSecurity
@PostMapping("/user")
public void addUser(@RequestBody UserDTO userDTO) {
    // Todo
}

  
在这个示例中,我们使用了AOP的方式来处理XSS过滤,通过自定义注解@XssSecurity来实现。在XssSecurityAspect的实现中,我们通过@Pointcut("@annotation(cn.example.demo.annotation.XssSecurity)")注解来定义Pointcut。在doBefore方法中,主要是通过反射的方式,对请求进行过滤。我们通过checkUrlExclude方法来判断当前请求是否在排除列表中,如果在就不进行过滤。

四、Spring-boot-starter-validation

1、Spring-boot-starter-validation介绍 Spring-boot-starter-validation是Springboot提供的JSR303验证依赖库,可以非常方便的对请求数据进行校验,包括数据类型、参数是否为空、参数是否在指定的范围内等等。使用起来非常方便简单。 2、Spring-boot-starter-validation示例 以下代码演示了如何使用Spring-boot-starter-validation的@NotBlank注解进行参数的非空判断:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
    @NotBlank(message = "用户名不能为空")
    private String name;
}

@RestController
public class UserController {
    @PostMapping("/user")
    public void addUser(@Validated @RequestBody UserDTO userDTO) {
        // Todo
    }
}
在这个示例中,我们使用了@NotBlank(message = "用户名不能为空")注解对UserDTO中的name字段进行了非空判断。如果请求中的name为空,Springboot会返回错误信息"用户名不能为空",并返回400响应码。

五、Spring Security过滤器链

1、Spring Security介绍 Spring Security是Spring官方的安全框架,提供了一系列的安全服务,包括权限认证、资源保护等,Spring Security内置了很多过滤器,可以通过配置来加入过滤器链进行XSS过滤。 2、Spring Security示例 以下代码演示了如何在Spring Security配置中添加XSS过滤器来保护应用:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public FilterRegistrationBean xssFilterRegistrationBean() {
        FilterRegistrationBean
    registrationBean = new FilterRegistrationBean
    ();
        registrationBean.setFilter(new XssFilter());
        registrationBean.addUrlPatterns("/*");
        registrationBean.setName("xssFilter");
        return registrationBean;
    }
}

    
   
  
我们在SecurityConfig中通过@Bean注解,加入了一个名为xssFilter的过滤器,把这个过滤器添加到了Spring Security的过滤器链中。这个过滤器实现方式与第一部分中的过滤器是一样的,都是通过XssHttpServletRequestWrapper来过滤请求。

六、小结

通过以上介绍,我们可以看出Springboot提供了多种方式进行XSS过滤,包括过滤器、注解、自定义注解和结合Spring Security等。不同的过滤方式有各自的优缺点和使用场景,开发人员可以结合具体需求来选择最合适的方式进行过滤。XSS攻击是一种非常常见且危险的攻击方式,合理的XSS过滤能够保障系统的安全稳定。