当前位置:   article > 正文

项目安全问题及解决方法-----xss处理

项目安全问题及解决方法-----xss处理

XSS 问题的根源在于,原本是让用户传入或输入正常数据的地方,被黑客替换为了 JavaScript 脚本,页面没有经过转义直接显示了这个数据,然后脚本就被 执行了。更严重的是,脚本没有经过转义就保存到了数据库中,随后页面加载数据的时候,数据中混入的脚本又当做代码执行了。黑客可以利用这个漏洞 来盗取敏感数据,诱骗用户访问钓鱼网站等。

  1. @RequestMapping("xss")
  2. @Slf4j
  3. @Controller
  4. public class XssController {
  5. @Autowired
  6. private UserRepository userRepository;
  7. //显示xss页面
  8. @GetMapping
  9. public String index(ModelMap modelMap) {
  10. //查数据库
  11. User user = userRepository.findById(1L).orElse(new User());
  12. //给View提供Model
  13. modelMap.addAttribute("username", user.getName());
  14. return "xss";
  15. }
  16. //保存用户信息
  17. @PostMapping
  18. public String save(@RequestParam("username") String username, HttpServletRequest request) {
  19. User user = new User();
  20. user.setId(1L);
  21. user.setName(username);
  22. userRepository.save(user);
  23. //保存完成后重定向到首页
  24. return "redirect:/xss/";
  25. }
  26. }
  27. //用户类,同时作为DTO和Entity
  28. @Entity
  29. @Data
  30. public class User {
  31. @Id
  32. private Long id;
  33. private String name;
  34. }

使用Thymeleaf 模板引擎来渲染页面

  1. <div style="font-size: 14px">
  2. <form id="myForm" method="post" th:action="@{/xss/}">
  3. <label th:utext="${username}"/> <!--对于 Thymeleaf 模板引擎,需要注意的是,使用 th:utext 来显示数据是不会进行转义的,需要使用 th:text-->
  4. <input id="username" name="username" size="100" type="text"/>
  5. <button th:text="Register" type="submit"/>
  6. </form>
  7. </div>

 

解决方法可以使用 HTML 转码。既然是通过 @RequestParam 来获取请求参数,那我们定义一个 @InitBinder 实现数据绑定的时候,对字符串进行转码即 可。

  1. @ControllerAdvice
  2. public class SecurityAdvice {
  3. @InitBinder
  4. protected void initBinder(WebDataBinder binder) {
  5. //注册自定义的绑定器
  6. binder.registerCustomEditor(String.class, new PropertyEditorSupport() {
  7. @Override
  8. public String getAsText() {
  9. Object value = getValue();
  10. return value != null ? value.toString() : "";
  11. }
  12. @Override
  13. public void setAsText(String text) {
  14. //赋值时进行HTML转义
  15. setValue(text == null ? null : HtmlUtils.htmlEscape(text));
  16. }
  17. });
  18. }
  19. }

 

但是解决问题的方式不全面,@InitBinder 是 Spring Web 层面的处理逻辑,如果有代码不通过 @RequestParam 来获取数据,而是直接从 HTTP 请求 获取数据的话,这种方式就不会奏效。比如: user.setName(request.getParameter("username")); 最好的解决方式是,定义一个 servlet Filter,通过 HttpServletRequestWrapper 实现 servlet 层面的统一参数替换。

  1. //自定义过滤器
  2. @Component
  3. @Order(Ordered.HIGHEST_PRECEDENCE)
  4. public class XssFilter implements Filter {
  5. @Override
  6. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletExceptio n {
  7. chain.doFilter(new XssRequestWrapper((HttpServletRequest) request), response);
  8. }
  9. }
  10. public class XssRequestWrapper extends HttpServletRequestWrapper {
  11. public XssRequestWrapper(HttpServletRequest request) {
  12. super(request);
  13. }
  14. @Override
  15. public String[] getParameterValues(String parameter) {
  16. //获取多个参数值的时候对所有参数值应用clean方法逐一清洁
  17. return Arrays.stream(super.getParameterValues(parameter)).map(this::clean).toArray(String[]::new);
  18. }
  19. @Override
  20. public String getHeader(String name) {
  21. //同样清洁请求头
  22. return clean(super.getHeader(name));
  23. }
  24. @Override
  25. public String getParameter(String parameter) {
  26. //获取参数单一值也要处理
  27. return clean(super.getParameter(parameter));
  28. }
  29. //clean方法就是对值进行HTML转义
  30. private String clean(String value) {
  31. return StringUtils.isEmpty(value)? "" : HtmlUtils.htmlEscape(value);
  32. }
  33. }

这种方式还是不够彻底,原因是无法处理通过 @RequestBody 注解提交的 JSON 数据。比如,有这样一个 PUT 接口,直接保存了客户端传入的 JSON User 对 象

  1. @PutMapping
  2. public void put(@RequestBody User user) {
  3. userRepository.save(user);
  4. }

 

因此我们需要自定义一个json的反序列器进行处理:

  1. //注册自定义的Jackson反序列器
  2. @Bean
  3. public Module xssModule() {
  4. SimpleModule module = new SimpleModule();
  5. module.module.addDeserializer(String.class, new XssJsonDeserializer());
  6. return module;
  7. }
  8. public class XssJsonDeserializer extends JsonDeserializer<String> {
  9. @Override
  10. public String deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
  11. String value = jsonParser.getValueAsString();
  12. if (value != null) {
  13. //对于值进行HTML转义
  14. return HtmlUtils.htmlEscape(value);
  15. }
  16. return value;
  17. }
  18. @Override
  19. public Class<String> handledType() {
  20. return String.class;
  21. }
  22. }

 这样就实现了既能转义 Get/Post 通过请求参数提交的数据,又能转义请求体中直接提交的 JSON 数据。但是目前这种只能堵新漏,确保新数据进入数据 库之前转义。如果因为之前的漏洞,数据库中已经保存了一些 JavaScript 代码,那么读取的时候同样可能出问题。因此,我们还要实现数据读取的时候也 转义。

  1. @GetMapping("user")
  2. @ResponseBody
  3. public User query() {
  4. return userRepository.findById(1L).orElse(new User());
  5. }

 

修改之前的 SimpleModule 加入自定义序列化器,并且实现序列化时处理字符串转义

  1. //注册自定义的Jackson序列器
  2. @Bean
  3. public Module xssModule() {
  4. SimpleModule module = new SimpleModule();
  5. module.addDeserializer(String.class, new XssJsonDeserializer());
  6. module.addSerializer(String.class, new XssJsonSerializer());
  7. return module;
  8. }
  9. public class XssJsonSerializer extends JsonSerializer<String> {
  10. @Override
  11. public Class<String> handledType() {
  12. return String.class;
  13. }
  14. @Override
  15. public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
  16. if (value != null) {
  17. //对字符串进行HTML转义
  18. jsonGenerator.writeString(HtmlUtils.htmlEscape(value));
  19. }
  20. }
  21. }

 

还要考虑一种情况:如果需要在 Cookie 中写入敏感信息的话,我们可以开启 HttpOnly 属性。这样 JavaScript 代码就无法读取 Cookie 了,即便页面被 XSS 注 入了攻击代码,也无法获得我们的 Cookie。

  1. //服务端读取Cookie
  2. @GetMapping("readCookie")
  3. @ResponseBody
  4. public String readCookie(@CookieValue("test") String cookieValue) {
  5. return cookieValue;
  6. }
  7. //服务端写入Cookie
  8. @GetMapping("writeCookie")
  9. @ResponseBody
  10. public void writeCookie(@RequestParam("httpOnly") boolean httpOnly, HttpServletResponse response) {
  11. Cookie cookie = new Cookie("test", "zhuye");
  12. //根据httpOnly入参决定是否开启HttpOnly属性
  13. cookie.setHttpOnly(httpOnly);
  14. response.addCookie(cookie);
  15. }

 由于 test 和 _ga 这两个 Cookie 不是 HttpOnly 的。通过 document.cookie 可以输出这两个 Cookie 的内容:

为 test 这个 Cookie 启用了 HttpOnly 属性后,就不能被 document.cookie 读取到了,输出中只有 _ga 一项:

 

 但是服务端可以读取到这个 cookie:

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号