SpringBoot 该如何预防 XSS 攻击

XSS 漏洞到底是什么,说实话我讲不太清楚。但是可以通过遇到的现象了解一下。在前端Form表单的输入框中,用户没有正常输入,而是输入了一段代码:</input><img src=1 onerror=alert1> 这个正常保存没有问题。问题出在了列表查询的时候,上面的代码就生效了,由于图片的地址乱写的,所以这个alert就起作用了来看图。

图片

 

那根据这个原理,实际上如果没有做任何的限制,有心人就可以为所欲为了。可以在里面嵌入一些关键代码,把你的信息拿走。确实是个很严重的问题。

解决思路

既然是因为输入框中输入了不该输入的东西,那自然就萌生一些想法:

  • 校验输入内容,不允许用户输入特殊字符,特殊标签

  • 允许用户输入,但是保存的时候将特殊的字符直接替换为空串

  • 允许用户输入,将特殊字符转译保存。

第一种方法,特殊字符过滤。既然要过滤特殊字符,那就得自己把所有的特殊字符列出来进行匹配,比较麻烦,而且要定义好什么才是特殊字符?况且用户本身不知道什么是特殊字符。突如其来的报错,会让用户有点摸不着头脑,不是很友好。

第二种方法,特殊字符替换为空串。未免有点太暴力。万一真的需要输入一点特殊的字符,保存完查出来发现少了好多东西,人家以为我们的BUG呢。也不是很好的办法。

第三种办法,特殊字符转译。这个办法不但用户数据不丢失,而且浏览器也不会执行代码。比较符合预期。

那办法确定了,怎么做呢?前端来做还是后端来做?想了想还是要后端来做。毕竟使用切面或者Filter可以一劳永逸。

心路历程

经过抄袭,我发现了一些问题,也渐渐的有了一些理解。下面再说几句废话:

查到的预防XSS攻击的,大多数的流程是:

  • 拦截请求

  • 重新包装请求

  • 重写HttpServletRequest中的获取参数的方法

  • 将获得的参数进行XSS处理

  • 拦截器放行

于是我就逮住一个抄了一下。抄袭完毕例行测试,发现我用@RequestBody接受的参数,并不能过滤掉特殊字符。怎么肥四?大家明明都这么写。为什么我的不好使?

这个时候突然一个想法萌生。SpringMVC在处理@RequestBody类型的参数的时候,是不是使用的我重写的这些方法呢?(getQueryString()getParameter(String name)getParameterValues(String name)getParameterMap())。打了个日志,发现还真不是这些方法。

于是搜索了一下Springboot拦截器获取@RequestBody参数,碰到了这篇文章。首先的新发现是Spring MVC 在获取@RequestBody参数的时候使用的是getInputStream()方法。嗯?(斜眼笑)那我是不是可以重写这个方法获取到输入流的字符串,然后直接处理一下?

说干就干,一顿操作。进行测试。发现直接JSON 转换的报错了。脑裂。估计是获得的字符串在转换的时候把不该转的东西转译了,导致不能序列化了。眼看就要成功了,一测回到解放前。

该怎么办呢?其实思路是没错的,就是在获取到流之后进行处理。但是错就错在处理的位置。果然处理的时间点很重要。(就像伴侣一样,某人出现的时间点很重要)。那既然不能在现在处理,那就等他序列化完毕之后再处理就好了。那怎么办呢?难道要写一个AOP 拦截到所有的请求?用JAVA反射处理?

正在迷茫的时候,看到了一篇文章,知识增加了。原来可以在序列化和反序列化的时候进行处理。

最终实现

看一下最终的代码实现(有些导入的包被我删了)

重新包装Request的代码
import org.apache.commons.text.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Map;/*** 重新包装一下Request。重写一些获取参数的方法,将每个参数都进行过滤*/
public class XSSHttpServletRequestWrapper extends HttpServletRequestWrapper {private static final Logger logger = LoggerFactory.getLogger(XSSHttpServletRequestWrapper.class);private HttpServletRequest request;/*** 请求体 RequestBody*/private String reqBody;/*** Constructs a request object wrapping the given request.** @param request The request to wrap* @throws IllegalArgumentException if the request is null*/public XSSHttpServletRequestWrapper(HttpServletRequest request) {super(request);logger.info("---xss XSSHttpServletRequestWrapper created-----");this.request = request;reqBody = getBodyString();}@Overridepublic String getQueryString() {return StringEscapeUtils.escapeHtml4(super.getQueryString());}/*** The default behavior of this method is to return getParameter(String* name) on the wrapped request object.** @param name*/@Overridepublic String getParameter(String name) {logger.info("---xss XSSHttpServletRequestWrapper work  getParameter-----");String parameter = request.getParameter(name);if (StringUtil.isNotBlank(parameter)) {logger.info("----filter before--name:{}--value:{}----", name, parameter);parameter = StringEscapeUtils.escapeHtml4(parameter);logger.info("----filter after--name:{}--value:{}----", name, parameter);}return parameter;}/*** The default behavior of this method is to return* getParameterValues(String name) on the wrapped request object.** @param name*/@Overridepublic String[] getParameterValues(String name) {logger.info("---xss XSSHttpServletRequestWrapper work  getParameterValues-----");String[] parameterValues = request.getParameterValues(name);if (!CollectionUtil.isEmpty(parameterValues)) {// 经 “@Belief_7” 指正 这种方式不能更改parameterValues里面的值,要换成下面👇的写法//for (String value : parameterValues) {//    logger.info("----filter before--name:{}--value:{}----", name, value);//    value = StringEscapeUtils.escapeHtml4(value);//    logger.info("----filter after--name:{}--value:{}----", name, value);// }for (int i = 0; i < parameterValues.length; i++) { parameterValues[i] = StringEscapeUtils.escapeHtml4(parameterValues[i]); } }return parameterValues;}/*** The default behavior of this method is to return getParameterMap() on the* wrapped request object.*/@Overridepublic Map<String, String[]> getParameterMap() {logger.info("---xss XSSHttpServletRequestWrapper work  getParameterMap-----");Map<String, String[]> map = request.getParameterMap();if (map != null && !map.isEmpty()) {for (String[] value : map.values()) {/*循环所有的value*/for (String str : value) {logger.info("----filter before--value:{}----", str, str);str = StringEscapeUtils.escapeHtml4(str);logger.info("----filter after--value:{}----", str, str);}}}return map;}/*重写输入流的方法,因为使用RequestBody的情况下是不会走上面的方法的*//*** The default behavior of this method is to return getReader() on the* wrapped request object.*/@Overridepublic BufferedReader getReader() throws IOException {logger.info("---xss XSSHttpServletRequestWrapper work  getReader-----");return new BufferedReader(new InputStreamReader(getInputStream()));}/*** The default behavior of this method is to return getInputStream() on the* wrapped request object.*/@Overridepublic ServletInputStream getInputStream() throws IOException {logger.info("---xss XSSHttpServletRequestWrapper work  getInputStream-----");/*创建字节数组输入流*/final ByteArrayInputStream bais = new ByteArrayInputStream(reqBody.getBytes(StandardCharsets.UTF_8));return new ServletInputStream() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener listener) {}@Overridepublic int read() throws IOException {return bais.read();}};}/*** 获取请求体** @return 请求体*/private String getBodyString() {StringBuilder builder = new StringBuilder();InputStream inputStream = null;BufferedReader reader = null;try {inputStream = request.getInputStream();reader = new BufferedReader(new InputStreamReader(inputStream));String line;while ((line = reader.readLine()) != null) {builder.append(line);}} catch (IOException e) {logger.error("-----get Body String Error:{}----", e.getMessage(), e);} finally {if (inputStream != null) {try {inputStream.close();} catch (IOException e) {logger.error("-----get Body String Error:{}----", e.getMessage(), e);}}if (reader != null) {try {reader.close();} catch (IOException e) {logger.error("-----get Body String Error:{}----", e.getMessage(), e);}}}return builder.toString();}
}
定义过滤器
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;/*** Filter 过滤器,拦截请求转换为新的请求*/
public class XssFilter implements Filter {private static final Logger logger = LoggerFactory.getLogger(XssFilter.class);/*** 初始化方法*/@Overridepublic void init(FilterConfig filterConfig) throws ServletException {logger.info("----xss filter start-----");}/*** 过滤方法*/@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {ServletRequest wrapper = null;if (request instanceof HttpServletRequest) {HttpServletRequest servletRequest = (HttpServletRequest) request;wrapper = new XSSHttpServletRequestWrapper(servletRequest);}if (null == wrapper) {chain.doFilter(request, response);} else {chain.doFilter(wrapper, response);}}
}
注册过滤器

注册过滤器我了解到的有两种方式。我用的下面的这种

一种通过@WebFilter注解的方式来配置,但这种启动类上要加@ServletComponentScan 注解来指定扫描路径

另外一种就是以Bean 的方式来注入(不知道放哪里,就把Bean放到启动类里面)

/*** XSS 的Filter注入* 用来处理getParameter的参数* @return*/
@Bean
public FilterRegistrationBean xssFilterRegistrationBean(){FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();filterRegistrationBean.setFilter(new XssFilter());filterRegistrationBean.setOrder(1);filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST);filterRegistrationBean.setEnabled(true);filterRegistrationBean.addUrlPatterns("/*");return filterRegistrationBean;
}

上面配的是使用request.getParameter()的时候生效的,但是当我使用@RequestBody来接收参数的时候是不行的,所以还得有下面的代码:

处理请求中的JSON数据
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.apache.commons.text.StringEscapeUtils;
import java.io.IOException;/*** 反序列化,用来处理请求中的JSON数据* 处理RequestBody方式接收的参数*/
public class XssJacksonDeserializer extends JsonDeserializer<String> {@Overridepublic String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {return StringEscapeUtils.escapeHtml4(jp.getText());}
}
处理返回值的JSON数据
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.apache.commons.text.StringEscapeUtils;
import java.io.IOException;/*** 处理向前端发送的JSON数据,将数据进行转译后发送*/
public class XssJacksonSerializer extends JsonSerializer<String> {@Overridepublic void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException {jgen.writeString(StringEscapeUtils.escapeHtml4(value));}
}
注册、配置自定义的序列化方法
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();ObjectMapper mapper = builder.build();/*注入自定义的序列化工具,将RequestBody的参数进行转译后传输*/SimpleModule simpleModule = new SimpleModule();// XSS序列化simpleModule.addSerializer(String.class, new XssJacksonSerializer());simpleModule.addDeserializer(String.class, new XssJacksonDeserializer());mapper.registerModule(simpleModule);converters.add(new MappingJackson2HttpMessageConverter(mapper));
}
测试

所有东西都配置完了,接下来进行愉快的测试阶段了。

我依然在输入框中输入这段代码</input><img src=1 onerror=alert1>并进行保存。来看一下数据库中的保存结果:

图片

图片

可以看到数据库中保存的数据,已经经过转译了。那查询一下列表是什么样的呢?

图片

可以看到两条数据,上面的是我们经过转译的,正常的展示出来了。而下面的是没经过转译的,直接空白,并且给我弹了个窗。

总结

  • 就是注意要分情况处理。

  • 拦截器处理一部分,并注意拦截器的注册方式

  • Jackson的方式处理另一部分,也是注意配置方式

补充

代码经过验证后,发现了一个问题。今天来补充一下。问题是这样的:

如果使用@RequestBody的形式接受参数,也就是需要使用自定义的序列化方式。然而有时候,我们的业务需要传递一些JSON串到后端,如{\"username\":\"zx\",\"pwd\":\"123\"}(注意这是个字符串)。但是因为我不管三七二十一直接暴力转译,导致里面的双引号以及其他符号都被转译了。那么当我们拿到这个字符串之后,再自己反序列化的时候就会出错了。

为了解决这个问题,我在自定义的序列化方法中判断了一下这个字段的值是否是JSON形式,如果是JSON形式,那就不做处理,直接返回,以保证能够顺利反序列化。判断是否是JSON的方式,我选择最简单的,判断首尾是否是{ } [ ]的组合。代码如下:

public class XssJacksonDeserializer extends JsonDeserializer<String> {@Overridepublic String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {// 判断一下 值是不是JSON的格式,如果是JSON的话,那就不处理了。/*判断JSON,可以用JSON.parse但是所有字段都Parse一下,未免有点太费性能,所以粗浅的认为,不是以{ 或者[ 开头的文本都不是JSON*/if (isJson(jp.getText())) {return jp.getText();}return StringEscapeUtils.escapeHtml4(jp.getText());}/*** 判断字符串是不是JSON** @param str* @return*/private boolean isJson(String str) {boolean result = false;if (StringUtil.isNotBlank(str)) {str = str.trim();if (str.startsWith("{") && str.endsWith("}")) {result = true;} else if (str.startsWith("[") && str.endsWith("]")) {result = true;}}return result;}
}

但是经过这样的改动之后,可能又没那么安全了。所以还是要看自己的取舍了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/39644.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

驱动 实现三个灯的亮灭

1、编写LED灯的驱动&#xff0c;可以控制三个灯&#xff0c;应用程序中编写控制灯的逻辑&#xff0c;要使用自动创建设备节点机制 head.h #ifndef __HEAD_H__ #define __HEAD_H__#define PHY_LED1_MODER 0x50006000 #define PHY_LED1_ODR 0x50006014 #define PHY_LED1_RCC 0x…

设计模式之责任链模式【Java实现】

责任链&#xff08;Chain of Resposibility&#xff09; 模式 概念 责任链&#xff08;chain of Resposibility&#xff09; 模式&#xff1a;为了避免请求发送者与多个请求处理者耦合在一起&#xff0c;于是将所有请求的处理者 通过前一对象记住其下一个对象的引用而连成一条…

【Python】使用python解析普通格式的报文为someip格式报文

文章目录 1.安装scapy库2.示例 1.安装scapy库 使用 pip 安装 scapy 第三方库&#xff0c;打开 cmd&#xff0c;输入以下命令&#xff1a; pip install scapy出现如图所示&#xff0c;表示安装成功&#xff1a; 2.示例 要解析someip格式报文&#xff0c;需要导入someip模块&a…

【Spring 】了解Spring AOP

目录 一、什么是Spring AOP 二、AOP的使用场景 三、AOP组成 四、Spring AOP的实现 1、添加Spring AOP依赖 2、定义切面和切点 3、定义相关通知 五、 AOP的实现原理 1、什么是动态代理 2、 JDK代理和CGLIB代理的区别 一、什么是Spring AOP AOP&#xff08;Aspect Ori…

PLY模型格式详解【3D】

本文介绍PLY 多边形文件格式&#xff0c;这是一种用于存储被描述为多边形集合的图形对象。 PLY文件格式的目标是提供一种简单且易于实现但通用的格式足以适用于各种模型。 PLY有两种子格式&#xff1a;易于入门的 ASCII 表示形式和用于紧凑存储和快速保存和加载的二进制格式。 …

【FastColoredTextBox】C# 开源文本编辑控件

主界面截图 使用Demos演示 FastColoredTextBox 是一个用于在 C# 程序中实现高亮语法着色、代码编辑和文本显示的自定义控件。它提供了许多功能&#xff0c;包括&#xff1a; 语法高亮&#xff1a;FastColoredTextBox 支持多种语言的语法高亮&#xff0c;可以根据语法规则将不同…

vite4+vue3+electron23.3+ts桌面应用bs端开发 打包windows、linux、max三个系统的安装包

vite4vue3electron23.3ts桌面应用bs端开发 打包windows、linux、max三个系统的安装包 主要包依赖 "electron-store": "^8.1.0", //全局数据状态管理&#xff0c;可选择性安装"electron": "23.3.8","electron-builder": &q…

网页显示摄像头数据的方法---基于web video server

1. 背景&#xff1a; 在ros系统中有发布摄像头的相关驱动rgb数据&#xff0c;需求端需要将rgb数据可以直接在网页上去显示。 问题解决&#xff1a; web_video_server功能包&#xff0c;相关链接&#xff1a; web_video_server - ROS Wiki 2. 下载&#xff0c;安装和编译&a…

Ubuntu20 ctrl+alt+T无法打开终端

事情是这样的&#xff0c;某天改了下python版本&#xff0c;发现linux默认打开终端的快捷键ctrlaltT寄了&#xff0c;网上给出的都是修改快捷键不出意外肯定没用 但是幸好我们是会分析的&#xff0c;我看到&#xff0c;很多回答说新增一个快捷键运行的命令是gnome-terminal&…

21、stm32使用LTDC驱动LCD

注&#xff1a;本文基于stm32使用FMC驱动SDRAM(IS42S32800G-6BLI)工程继续开发 本例使用安富莱的H743XIH板子驱动LTDC点亮7寸LCD 硬件接线&#xff1a;RGB888 一、cubemx配置 1、LTDC配置 注意此引脚应于上面的硬件接线图一致 2、配置DMA2D 3、背光引脚和触摸引脚 4、时钟…

在 IntelliJ IDEA 中使用 Docker 开发指南

目录 一、IDEA安装Docker插件 二、IDEA连接Docker 1、Docker for Windows 连接 2、SSH 连接 3、Connection successful 连接成功 三、查看Docker面板 四、使用插件生成镜像 一、IDEA安装Docker插件 打开 IntelliJ IDEA&#xff0c;点击菜单栏中的 "File" -&g…

LeetCode 778. Swim in Rising Water【最小瓶颈路;二分+BFS或DFS;计数排序+并查集;最小生成树】2096

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

cs231n assignment 3 Q2 Image Captioning with Vanilla RNNs

文章目录 嫌啰嗦直接看代码Q2 Image Captioning with Vanilla RNNs一个给的工具代码里的bug问题展示问题解决思路解决办法 rnn_step_forward题面解析代码输出 rnn_step_backward题面解析代码输出 rnn_forward题面解析代码输出 rnn_backward题面解析代码输出 word_embedding_for…

使用 BERT 进行文本分类 (02/3)

​ 一、说明 在使用BERT&#xff08;1&#xff09;进行文本分类中&#xff0c;我向您展示了一个BERT如何标记文本的示例。在下面的文章中&#xff0c;让我们更深入地研究是否可以使用 BERT 来预测文本是使用 PyTorch 传达积极还是消极的情绪。首先&#xff0c;我们需要准备数据…

3.1 Qt样式选择器

本期内容 3.1 样式选择器 3.1.1 Universal Selector (通用选择器) 3.1.2 Type Selector (类型选择器) 3.1.3 Property Selector (属性选择器) 3.1.4 Class Selector (类选择器) 3.1.5 ID Selector (ID选择器) 3.1.6 Descendant Selector (后裔选择器) 3.1.7 Chil…

前端跨域的原因以及解决方案(vue),一文让你真正理解跨域

跨域这个问题,可以说是前端的必需了解的,但是多少人是知其然不知所以然呢&#xff1f; 下面我们来梳理一下vue解决跨域的思路。 什么情况会跨域&#xff1f; ​ 跨域的本质就是浏览器基于同源策略的一种安全手段。所谓同源就是必须有以下三个相同点&#xff1a;协议相同、域名…

WinCC V7.5 中的C脚本对话框不可见,将编辑窗口移动到可见区域的具体方法

WinCC V7.5 中的C脚本对话框不可见&#xff0c;将编辑窗口移动到可见区域的具体方法 由于 Windows 系统更新或使用不同的显示器&#xff0c;在配置C动作时&#xff0c;有可能会出现C脚本编辑窗口被移动到不可见区域的现象。 由于该窗口无法被关闭&#xff0c;故无法进行进一步…

KafkaStream:Springboot中集成

1、在kafka-demo中创建配置类 配置kafka参数 package com.heima.kafkademo.config;import lombok.Data; import org.apache.kafka.common.serialization.Serdes; import org.apache.kafka.streams.StreamsConfig; import org.springframework.boot.context.properties.Configu…

8月11日上课内容 nginx的多实例和动静分离

多实例部署 在一台服务器上有多个tomcat的服务。 配置多实例之前&#xff0c;看单个实例是否访问正常。 1.安装好 jdk 2.安装 tomcat cd /opt tar zxvf apache-tomcat-9.0.16.tar.gz mkdir /usr/local/tomcat mv apache-tomcat-9.0.16 /usr/local/tomcat/tomcat1 cp -a /u…

Linux系统管理:虚拟机ESXi安装

目录 一、理论 1.VMware Workstation 2.VMware vSphere Client 3.ESXi 二、实验 1.ESXi 7安装 一、理论 1.VMware Workstation 它是一款专业的虚拟机软件&#xff0c;可以在一台物理机上运行多个操作系统&#xff0c;支持Windows、Linux等操作系统&#xff0c;可以模拟…