xss攻击的解决过程
1.添加一个拦截器,在拦截器中对request中的body体进行读取
2.对body体中的特殊字符进行转码或者进行过滤
3.将处理过的body重新塞回request
具体操作可参考:https://www.cnblogs.com/0201zcr/p/13143165.html
在实际项目中,我们的接口支持json中携带注释,根据以上操作添加拦截器之后,调用携带注释的请求体一直报错;
入参如下:
过滤器中代码如下:
package com.insgeek.boot.web.filter; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.http.Header; import cn.hutool.http.HtmlUtil; import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.collect.Maps; import com.insgeek.boot.commons.json.JacksonUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ReadListener; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; /** * 防止xss攻击 * @author holley */ @Slf4j @Component public class XssFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { filterChain.doFilter(new XssHttpServletRequestWrapper(request),response); } public static class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { public XssHttpServletRequestWrapper(HttpServletRequest request) { super(request); } /** * 重写getParameter方法,用HtmlUtil转义后再返回 */ @Override public String getParameter(String name) { String value= super.getParameter(name); if(!CharSequenceUtil.hasEmpty(value)){ value= HtmlUtil.filter(value); } return value; } /** * 重写getParameterValues方法, * 遍历每一个值,用HtmlUtil转义后再返回 */ @Override public String[] getParameterValues(String name) { String[] values= super.getParameterValues(name); if(values!=null){ for (int i=0;i<values.length;i++){ String value=values[i]; if(!CharSequenceUtil.hasEmpty(value)){ value=HtmlUtil.filter(value); } values[i]=value; } } return values; } /** * 重写getParameterMap方法, * 拿到所有的k-v键值对,用LinkedHashMap接收, * key不变,value用HtmlUtil转义后再返回 */ @Override public Map<String, String[]> getParameterMap() { Map<String, String[]> parameters = super.getParameterMap(); LinkedHashMap<String, String[]> map= Maps.newLinkedHashMap(); if(parameters!=null){ for (String key:parameters.keySet()){ String[] values=parameters.get(key); for (int i = 0; i < values.length; i++) { String value = values[i]; if (!CharSequenceUtil.hasEmpty(value)) { value = HtmlUtil.filter(value); } values[i] = value; } map.put(key,values); } } return map; } /** * 重写getHeader方法,用HtmlUtil转义后再返回 */ @Override public String getHeader(String name) { String value= super.getHeader(name); if (!CharSequenceUtil.hasEmpty(value)) { value = HtmlUtil.filter(value); } return value; } @Override public ServletInputStream getInputStream() throws IOException { /** * 拿到数据流,通过StringBuffer拼接, * 读取到line上,用StringBuffer是因为会有多个线程同时请求,要保证线程的安全 */ InputStream in= super.getInputStream(); InputStreamReader reader=new InputStreamReader(in); BufferedReader buffer=new BufferedReader(reader); StringBuffer body = new StringBuffer(); // 注意问题出在这里!!!! String line=buffer.readLine(); while(line!=null){ body.append(line); line=buffer.readLine(); } buffer.close(); reader.close(); in.close(); /** * 将拿到的map,转移后存到另一个map中 */ // Map<String,Object> map= JacksonUtils.readValue(body.toString(), new TypeReference<Map<String, Object>>() {}); // Map<String,Object> result=new LinkedHashMap<>(); // for(String key:map.keySet()){ // Object val=map.get(key); // if(val instanceof String){ // if(!CharSequenceUtil.hasEmpty(val.toString())){ // result.put(key,HtmlUtil.filter(val.toString())); // } // }else { // result.put(key,val); // } // } // String json = JacksonUtils.writeAsString(result); InputStream bain = new ByteArrayInputStream(body.toString().getBytes()); //匿名内部类,只需要重写read方法,把转义后的值,创建成ServletInputStream对象 return new ServletInputStream() { @Override public int read() throws IOException { return bain.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } } }
在过滤器中只是将流转成字符串,然后再转回流,中间没有其它操作,结果在后面的AbstractJackson2HttpMessageConverter类中解析json进行实体映射时,抛出异常
JSON parse error: Unexpected end-of-input: ... PushbackInputStream ... JsonEOFException
最后排查发现:在使用bufferReader读取流时,使用readline方法会忽略换行符,导致最后生成的字符串变成了一行,即:{ "action_type":1, // "entity_key": "qp_quote_info", "entity_key": "ig_claim_list", // "entity_key": "ig_insurance_apply", // "entity_key": "qp_quote", // "entity_key": "holley", "data_id": "1500000", // "assigner":1997, "variables": { // "name2":"aaa", // "name":"Holley1", // "name4":"Holley4" // "channel_id":"1234" // 渠道id }}。也就是说:从第一个// 之后,所有的数据都读去成注释了,导致json转换失败
解决方案:使用ByteArrayOutputStream读取数据进行转换,保留换行符
@Override public ServletInputStream getInputStream() throws IOException { InputStream in= super.getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int len = -1; byte[] buffer = new byte[1024];//1kb while ((len = in.read(buffer)) != -1) { baos.write(buffer, 0, len); } in.close(); String json = new String(baos.toByteArray()); InputStream bain = new ByteArrayInputStream(XssUtil.cleanXss(json).getBytes()); //匿名内部类,只需要重写read方法,把转义后的值,创建成ServletInputStream对象 return new ServletInputStream() { @Override public int read() throws IOException { return bain.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; }