XSS攻击过滤器实现

一、XSS简介
百度百科的解释: XSS又叫CSS  (Cross Site Script) ,跨站脚本攻击。它指的是恶意攻击者往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意用户的特殊目的。

它与SQL注入攻击类似,SQL注入攻击中以SQL语句作为用户输入,从而达到查询/修改/删除数据的目的,而在xss攻击中,通过插入恶意脚本,实现对用户游览器的控制,获取用户的一些信息。

二、XSS分类

xss攻击可以分成两种类型:

  1. 非持久型攻击
  2. 持久型攻击

非持久型xss攻击:顾名思义,非持久型xss攻击是一次性的,仅对当次的页面访问产生影响。非持久型xss攻击要求用户访问一个被攻击者篡改后的链接,用户访问该链接时,被植入的攻击脚本被用户游览器执行,从而达到攻击目的。

持久型xss攻击:持久型xss,会把攻击者的数据存储在服务器端,攻击行为将伴随着攻击数据一直存在。

也可以分成三类:

  1. 反射型:经过后端,不经过数据库
  2. 存储型:经过后端,经过数据库
  3. DOM:不经过后端,DOM—based XSS漏洞是基于文档对象模型Document Objeet Model,DOM)的一种漏洞,dom - xss是通过url传入参数去控制触发的

三、代码实现

1、导入pom文件依赖

<!-- 添加StringEscapeUtils.escapeHtml4()工具类过时方法的替代类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-text</artifactId>
            <version>1.9</version>
        </dependency>

2、首先是要写个过滤器的包装类,这也是实现XSS攻击过滤的核心代码。

根据业务需要,我这里写好了两个XSS包装类,根据需要选择使用

第一种写法:

package com.evan.example.xss.filter;


import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;

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.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @author liudg
 * @createTime 2021/7/12 下午4:59
 * @Description XSS过滤具体核心代码(第一种)
 */
public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrapper {

    //声明sql注入的关键词key
    private static String key = "and|exec|insert|select|delete|update|count|*|%|chr|mid|master|truncate|char|declare|;|or|-|+";
    private static Set<String> notAllowedKeyWords = new HashSet<String>(0);
    private static String replacedString = "INVALID";

    static {
        String keyStr[] = key.split("\\|");
        //将key添加到Set集合中
        for (String str : keyStr) {
            notAllowedKeyWords.add(str);
        }
    }

    /**
     * 构造函数,传入参数,执行超类
     *
     * @param request
     */
    public XssAndSqlHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    /**
     * 重写getParameter方法 ,getParameter方法是直接通过request获得querystring类型的入参调用的方法
     *
     * @param name
     * @return
     */
    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        if (!StringUtils.isEmpty(value)) {
            //调用Apache的工具类:StringEscapeUtils.escapeHtml4
            value = StringEscapeUtils.escapeHtml4(value);
            value = cleanSqlKeyWords(value);
        }
        return value;
    }

    /**
     * 重写getParameterValues
     *
     * @param name
     * @return
     */
    @Override
    public String[] getParameterValues(String name) {
        String[] parameterValues = super.getParameterValues(name);
        if (parameterValues == null) {
            return null;
        }
        for (int i = 0; i < parameterValues.length; i++) {
            String value = parameterValues[i];
            //调用Apache的工具类:StringEscapeUtils.escapeHtml4
            parameterValues[i] = StringEscapeUtils.escapeHtml4(value);
            parameterValues[i] = cleanSqlKeyWords(parameterValues[i]);
        }
        return parameterValues;
    }

    @Override
    public String getHeader(String name) {
        //过滤xss攻击
        String value = StringEscapeUtils.escapeHtml4(super.getHeader(name));
        if (value == null) {
            return null;
        }
        //过滤sql注入
        return cleanSqlKeyWords(value);
    }

    @Override
    public String getQueryString() {
        return StringEscapeUtils.escapeHtml4(super.getQueryString());
    }

    /**
     * 过滤JSON数据中的XSS攻击
     *
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        //调用方法将流数据return为String
        String str = getRequestBody(super.getInputStream());
        //如果str为"",则返回0
        if ("".equals(str)) {
            return new ServletInputStream() {
                @Override
                public int read() throws IOException {
                    return 0;
                }

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

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

                @Override
                public void setReadListener(ReadListener readListener) {

                }
            };
        }
        //将数据存放至map
        Map<String, Object> map = JSON.parseObject(str, Map.class);
        //声明个存放过滤后数据的hashMap
        Map<String, Object> resultMap = new HashMap<>(map.size());
        //开始遍历数据
        for (String key : map.keySet()) {
            Object val = map.get(key);
            //如果key=富文本字段名,就不去过滤
            if ("content".equals(key)) {
                //不过滤
                resultMap.put(key, val);
            } else {
                //不为富文本字段才会过滤
                if (map.get(key) instanceof String) {
                    //通过escapeHtml4去过滤
                    resultMap.put(key, StringEscapeUtils.escapeHtml4(cleanSqlKeyWords(val.toString())));
                } else {
                    //不过滤
                    resultMap.put(key, val);
                }
            }
        }
        str = JSON.toJSONString(resultMap);
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(str.getBytes());
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

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

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

            @Override
            public void setReadListener(ReadListener readListener) {

            }

        };
    }

    /**
     * 获取JSON数据
     *
     * @param stream
     * @return
     */
    private String getRequestBody(InputStream stream) {
        String line = "";
        StringBuilder body = new StringBuilder();
        int counter = 0;
        // 读取POST提交的数据内容
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream, Charset.forName("UTF-8")));
        try {
            while ((line = reader.readLine()) != null) {
                //拼接读取到的数据
                body.append(line);
                counter++;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (body == null) {
            return "";
        }
        //最后返回数据
        return body.toString();
    }


    /**
     * 过滤可能造成sql注入的关键字
     *
     * @param value
     * @return
     */
    private String cleanSqlKeyWords(String value) {
        String paramValue = value;
        for (String keyword : notAllowedKeyWords) {
            if (paramValue.length() > keyword.length() + 4
                    && (paramValue.contains(" " + keyword) || paramValue.contains(keyword + " ") || paramValue.contains(" " + keyword + " "))) {
                paramValue = StringUtils.replace(paramValue, keyword, replacedString);
            }
        }
        return paramValue;
    }

}


第二种写法:

package com.evan.example.xss.filter;

import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @author liudg
 * @createTime 2021/7/12 下午5:24
 * @Description xss过滤核心代码(第二种)
 */
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private static String key = "and|exec|insert|select|delete|update|count|*|%|chr|mid|master|truncate|char|declare|;|or|-|+";
    private static Set<String> notAllowedKeyWords = new HashSet<String>(0);
    private static String replacedString = "INVALID";

    static {
        String keyStr[] = key.split("\\|");
        for (String str : keyStr) {
            notAllowedKeyWords.add(str);
        }
    }

    private String currentUrl;
    private byte[] body;

    /**
     * 构造函数
     * @param servletRequest
     * @throws IOException
     */
    public XssHttpServletRequestWrapper(HttpServletRequest servletRequest) throws IOException {
        super(servletRequest);
        currentUrl = servletRequest.getRequestURI();
        //获取到json数据
        this.body = StreamUtils.copyToByteArray(servletRequest.getInputStream());
    }

    /**
     * 重写该方法过滤json数据
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        ServletInputStream inputStream = null;
        String bodyStr = new String(body);
        if (!StringUtils.isEmpty(bodyStr)) {
            bodyStr = xssEncode(bodyStr, 1);
            bodyStr = cleanSqlKeyWords(bodyStr);
            final ByteArrayInputStream bais = new ByteArrayInputStream(bodyStr.getBytes());
            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 bais.read();
                }
            };
        }
        return inputStream;
    }

    /**
     * 将容易引起xss漏洞的半角字符直接替换成全角字符
     * @param s
     * @param type
     * @return
     */
    private static String xssEncode(String s, int type) {
        if (s == null || s.isEmpty()) {
            return s;
        }
        StringBuilder sb = new StringBuilder(s.length() + 16);
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (type == 0) {
                switch (c) {
                    case '\'':
                        // 全角单引号
                        sb.append('‘');
                        break;
                    case '\"':
                        // 全角双引号
                        sb.append('“');
                        break;
                    case '>':
                        // 全角大于号
                        sb.append('>');
                        break;
                    case '<':
                        // 全角小于号
                        sb.append('<');
                        break;
                    case '&':
                        // 全角&符号
                        sb.append('&');
                        break;
                    case '\\':
                        // 全角斜线
                        sb.append('\');
                        break;
                    case '#':
                        // 全角井号
                        sb.append('#');
                        break;
                    // < 字符的 URL 编码形式表示的 ASCII 字符(十六进制格式) 是: %3c
                    case '%':
                        processUrlEncoder(sb, s, i);
                        break;
                    default:
                        sb.append(c);
                        break;
                }
            } else {
                switch (c) {
                    case '>':
                        // 全角大于号
                        sb.append('>');
                        break;
                    case '<':
                        // 全角小于号
                        sb.append('<');
                        break;
                    case '&':
                        // 全角&符号
                        sb.append('&');
                        break;
                    case '#':
                        // 全角井号
                        sb.append('#');
                        break;
                    // < 字符的 URL 编码形式表示的 ASCII 字符(十六进制格式) 是: %3c
                    case '%':
                        processUrlEncoder(sb, s, i);
                        break;
                    default:
                        sb.append(c);
                        break;
                }
            }

        }
        return sb.toString();
    }

    public static void processUrlEncoder(StringBuilder sb, String s, int index) {
        if (s.length() >= index + 2) {
            // %3c, %3C
            if (s.charAt(index + 1) == '3' && (s.charAt(index + 2) == 'c' || s.charAt(index + 2) == 'C')) {
                sb.append('<');
                return;
            }
            // %3c (0x3c=60)
            if (s.charAt(index + 1) == '6' && s.charAt(index + 2) == '0') {
                sb.append('<');
                return;
            }
            // %3e, %3E
            if (s.charAt(index + 1) == '3' && (s.charAt(index + 2) == 'e' || s.charAt(index + 2) == 'E')) {
                sb.append('>');
                return;
            }
            // %3e (0x3e=62)
            if (s.charAt(index + 1) == '6' && s.charAt(index + 2) == '2') {
                sb.append('>');
                return;
            }
        }
        sb.append(s.charAt(index));
    }


    /**
     * 覆盖getParameter方法,将参数名和参数值都做xss过滤。
     * 如果需要获得原始的值,则通过super.getParameterValues(name)来获取
     * getParameterNames,getParameterValues和getParameterMap也可能需要覆盖
     */
    @Override
    public String getParameter(String parameter) {
        String value = super.getParameter(parameter);
        if (value == null) {
            return null;
        }
        return cleanXSS(value);
    }

    @Override
    public String[] getParameterValues(String parameter) {
        String[] values = super.getParameterValues(parameter);
        if (values == null) {
            return null;
        }
        int count = values.length;
        String[] encodedValues = new String[count];
        for (int i = 0; i < count; i++) {
            encodedValues[i] = cleanXSS(values[i]);
        }
        return encodedValues;
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> values = super.getParameterMap();
        if (values == null) {
            return null;
        }
        Map<String, String[]> result = new HashMap<>();
        for (String key : values.keySet()) {
            String encodedKey = cleanXSS(key);
            int count = values.get(key).length;
            String[] encodedValues = new String[count];
            for (int i = 0; i < count; i++) {
                encodedValues[i] = cleanXSS(values.get(key)[i]);
            }
            result.put(encodedKey, encodedValues);
        }
        return result;
    }

    /**
     * 覆盖getHeader方法,将参数名和参数值都做xss过滤。
     * 如果需要获得原始的值,则通过super.getHeaders(name)来获取
     * getHeaderNames 也可能需要覆盖
     */
    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        if (value == null) {
            return null;
        }
        return cleanXSS(value);
    }

    private String cleanXSS(String valueP) {
        String value = valueP.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
        value = value.replaceAll("<", "& lt;").replaceAll(">", "& gt;");
        value = value.replaceAll("\\(", "& #40;").replaceAll("\\)", "& #41;");
        value = value.replaceAll("'", "& #39;");
        value = value.replaceAll("eval\\((.*)\\)", "");
        value = value.replaceAll("[\\\"\\\'][\\s]*javascript:(.*)[\\\"\\\']", "\"\"");
        value = value.replaceAll("script", "");
        value = cleanSqlKeyWords(value);
        return value;
    }

    private String cleanSqlKeyWords(String value) {
        String paramValue = value;
        for (String keyword : notAllowedKeyWords) {
            if (paramValue.length() > keyword.length() + 3
                    && (paramValue.contains(" " + keyword) || paramValue.contains(keyword + " ") || paramValue.contains(" " + keyword + " "))) {
                paramValue = StringUtils.replace(paramValue, keyword, replacedString);
                System.out.println(this.currentUrl + "已被过滤,因为参数中包含不允许sql的关键词(" + keyword
                        + ")" + ";参数:" + value + ";过滤后的参数:" + paramValue);
            }
        }
        return paramValue;
    }

}

3、写一个过滤器

package com.evan.example.xss.filter;

/**
 * @author liudg
 * @createTime 2021/7/12 下午5:29
 * @Description
 */
import com.evan.example.xss.filter.XssAndSqlHttpServletRequestWrapper;
import com.evan.example.xss.filter.XssHttpServletRequestWrapper;
import org.springframework.stereotype.Component;

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

/**
 * @author liudg
 * @createTime 2021/7/12 下午5:27
 * @Description xss攻击过滤器防御
 */
@WebFilter
@Component
public class XssFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        //获取请求数据
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        //获取请求的url路径
        String path = ((HttpServletRequest) servletRequest).getServletPath();
        //声明要被忽略请求的数组
        String[] exclusionsUrls = {".js", ".gif", ".jpg", ".jpeg", ".png", ".css", ".ico", "health", "uploadPic", "file", "savePrintTemp", "goods/create", "goods/edit", "goodsBatchImport", "goodsBatchExport","platformtestreport"};
        //声明带有富文本的接口数组
        String[] richTextUrls = {"addarticles", "editarticles", "advert"};
        //第一种xss过滤
        XssAndSqlHttpServletRequestWrapper XssAndSqlHttpServletRequestWrapper = new XssAndSqlHttpServletRequestWrapper(req);
        //遍历忽略的请求数组,若该接口url为忽略的就调用原本的过滤器,不走xss过滤
        for (String str : exclusionsUrls) {
            if (path.contains(str)) {
                filterChain.doFilter(servletRequest, servletResponse);
                return;
            }
        }
        //若为带有富文本的接口,走第一种xss过滤
        for (String rtu : richTextUrls) {
            if (path.contains(rtu)) {
                filterChain.doFilter(XssAndSqlHttpServletRequestWrapper, servletResponse);
                return;
            }
        }
        //将请求放入XSS请求包装器中,返回过滤后的值
        XssHttpServletRequestWrapper xssHttpServletRequestWrapper = new XssHttpServletRequestWrapper(req);
        filterChain.doFilter(xssHttpServletRequestWrapper, servletResponse);
    }

    @Override
    public void destroy() {

    }

}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值