独立完成系统开发九:安全问题

独立完成系统开发九:安全问题

这篇博文我会介绍一下系统中会碰到的一些安全问题,以及漏洞的处理,如果后面遇到了其他的安全问题后面会持续更新

XSS(跨站脚本攻击)

xss介绍

跨站脚本攻击在实际中还是很常见的。而且实现XSS攻击还并不是很复杂,所以还是很有必要对XSS进行处理的

先介绍一下什么是XSS:

跨站脚本攻击(Cross Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。

你可以自己做个简单尝试:

  1. 在任何一个表单内,你输入一段简单的js代码:<script>for(var i=0;i<1000;i++){alert("弹死你"+i);}</script>,将其存入数据库;
  2. 在页面上一个div元素内直接展示第一步内存入的值,你会发现弹出框出现了;

以上XSS攻击只算一个小恶作剧,但如果这玩意被发到了网站的首页上,我估计老板一定会因为频繁的投诉而和你来场愉快的谈话…

当然除了这种恶作剧,恶意用户能做的更多,如获取用户信息,进行“网络钓鱼”攻击等。

应对XSS攻击的其中一个方式就是后端对输入内容进行过滤,输入内容里面的敏感信息直接过滤,如<script>标签等。

在普通的web项目中会出现XSS攻击,那么在vue中是否会存在这个问题呢,答案是肯定的,只不过在在vue中只有在使用v-html指令的时候才可能会出现XSS攻击。

具体实验可以参考这篇文章:https://blog.sqreen.com/xss-in-vue-js/

xss处理

对xss处理其实也很简单,无非就是在请求到达后端之后,我们将请求拦截然后对的请求参数进行特殊的处理过滤掉一些敏感的字符就可以了。至于如何过滤这些敏感的字符,我们可以直接写正则,不过这个很明显是吃力不讨好的,这里介绍一个比较好用的工具Jsoup,注意不是json哈

那么jsoup是什么呢,我们来看jsoup官网的介绍:

jsoup实现WHATWG HTML5规范,并将HTML解析为与现代浏览器相同的DOM。

  • 从URL,文件或字符串中刮取和解析 HTML
  • 使用DOM遍历或CSS选择器查找和提取数据
  • 操纵 HTML元素,属性和文本
  • 清除用户提交的内容以防止安全白名单,以防止XSS攻击
  • 输出整洁的HTML

maven依赖:

<dependency>
  <groupId>org.jsoup</groupId>
  <artifactId>jsoup</artifactId>
  <version>{jsoup.version}</version>
</dependency>
具体实现

下面先说一下处理的思路,我们都知道后台获取请求参数都是在ServletRequest中获取的,所以我们可以写一个过滤器在请求到达业务处理之前对request进行包装重写它里面的方法,在获取请求数据的时候在我们的包装类中对请求的数据进行过滤。这样获取到的数据就是过滤后的数据了。

首先编写我们的过滤器,对request进行包装:

public class XssFilter implements Filter {

    private static Logger logger = LoggerFactory.getLogger(XssFilter.class);


    public List<String> excludes = new ArrayList<String>();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException,ServletException {
        logger.debug("xss filter is open");
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        //看是否为需要排除的url
        if(handleExcludeUrl(req, resp)){
            filterChain.doFilter(request, response);
            return;
        }
        //将request包装为对Xss过滤的request
        XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
        filterChain.doFilter(xssRequest, response);
    }

    /**
     * 功能描述: 是否有需要排除的url
     * @param request, response
     * @return boolean
     * @author cdfan
     * @date 2020/4/23 14:00
     */
    private boolean handleExcludeUrl(HttpServletRequest request, HttpServletResponse response) {
        if (excludes == null || excludes.isEmpty()) {
            return false;
        }
        String url = request.getServletPath();
        for (String pattern : excludes) {
            Pattern p = Pattern.compile("^" + pattern);
            Matcher m = p.matcher(url);
            if (m.find()) {
                return true;
            }
        }
        return false;
    }

    /**
     * 功能描述: 初始化操作将,不需要过滤的url排除
     * @author cdfan
     * @date 2020/5/22 17:30
     */
    @Override
    public void init(FilterConfig filterConfig) {
        logger.debug("xss filter init~~~~~~~~~~~~");
        String temp = filterConfig.getInitParameter("excludes");
        if (temp != null) {
            String[] url = temp.split(",");
            if (ObjectUtils.isNotEmpty(url)){
                Collections.addAll(excludes, url);
            }
        }
    }

    @Override
    public void destroy() {}

}

request包装类:

public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
    /**
     * 原始request
     */
    private HttpServletRequest orgRequest = null;
    /**
     *判断是否是上传 上传忽略
     */
    private boolean isUpData = false;

    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
        orgRequest = request;
        String contentType = request.getContentType();
        if (null != contentType) {
            isUpData = contentType.startsWith("multipart");
        }
    }

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

    /**
     * 功能描述: 覆盖getParameterValues 方法
     * @param name param的名称
     * @return java.lang.String[]
     * @author cdfan
     * @date 2020/5/22 17:32
     */
    @Override
    public String[] getParameterValues(String name) {
        String[] arr = super.getParameterValues(name);
        if(arr != null){
            for (int i=0;i<arr.length;i++) {
                arr[i] = JsoupUtil.clean(arr[i]);
            }
        }
        return arr;
    }


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




    /**
     * 功能描述: 处理@RequestBody获取的值
     * @return javax.servlet.ServletInputStream
     * @author cdfan
     * @date 2020/4/23 15:23
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (isUpData) {
            return super.getInputStream();
        } else {
            //处理原request的流中的数据
            byte[] bytes = inputHandlers(super.getInputStream()).getBytes();
            final ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
            return new ServletInputStream() {
                @Override
                public int read() {
                    return bais.read();
                }
                @Override
                public boolean isFinished() {
                    return false;
                }

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

                @Override
                public void setReadListener(ReadListener readListener) {
                }
            };
        }

    }

    /**
     * 功能描述: 对原request的流中的数据进行Xss处理
     */
    private String inputHandlers(ServletInputStream servletInputStream) {
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(servletInputStream, Charset.forName("UTF-8")));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (servletInputStream != null) {
                try {
                    servletInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return JsoupUtil.clean(sb.toString());
    }

    /**
     * 获取最原始的request
     *
     */
    public HttpServletRequest getOrgRequest() {
        return this.orgRequest;
    }

}

注意这里如果是get请求直接获取数据然后通过jsoup进行过滤就可以了,但是如果请求是post或put类型的请求,那么请求的数据是以流的形式存在的,所以我们从request中获取的数据的时候,需要先读取流,然后转为字符串然后再将字符串传递给jsoup进行处理,并且在request中由于请求流只能读取一次,所以处理完之后我们需要在重新返回一个新的ServletInputStream,在ServletInputStream中返回我们处理之后的流数据。

jsoup过滤工具类:

public class JsoupUtil {

    /**
     * 使用自带的relaxed 白名单
     * 允许的便签有a,b,blockquote,br,caption,cite,code,col,colgroup,dd,div,dl,dt,em,h1,h2,h3,h4,h5,h6,i,img,li,
     * ol,p,pre,q,small,span,strike,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,u,ul
     */
    private static final Whitelist WHITELIST = Whitelist.relaxed();
    /** 配置过滤化参数,不对代码进行格式化 */
    private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
    static {
      	// 添加白名单
        // 富文本编辑时一些样式是使用style来进行实现的
        // 比如红色字体 style="color:red;"
        // 所以需要给所有标签添加style属性, 注意style中的属性之间不能有空格
        WHITELIST.addAttributes(":all", "style");
        //WHITELIST.addAttributes("a","href","class");
    }

    public static String clean(String content) {
        String str = Jsoup.clean(content, "", WHITELIST, OUTPUT_SETTINGS);
        //处理自动转义以及额外添加引号,导致json字符串无法转换为json对象的问题
        return StringEscapeUtils.unescapeHtml4(str).replaceAll("\"\\\\","\\\\").replaceAll("\\\\\"\"","\\\\\"");
    }


    public static void main(String[] args) throws FileNotFoundException, IOException {
        String text = "{\"roleId\":3,\"roleName\":\"业务角色\",\"roleCode\":\"bus\",\"remark\":\"<a class=\\\"class1\\\" style=\\\"width:100px;height:50px;\\\">测试</a>\",\"menuTree\":[{\"children\":[{\"menuId\":7,\"pid\":6,\"menuName\":\"业务一\"}],\"menuId\":6,\"menuName\":\"业务管理\"}],\"allMenuIds\":null}";
        System.out.println(text);
        System.out.println(Jsoup.clean(text, "", WHITELIST, OUTPUT_SETTINGS));
        System.out.println(clean(text));
        Role role = JSON.parseObject(clean(text), Role.class);
        System.out.println(role);
    }
}

这里Whitelist.relaxed()是指标签的白名单, 在这个白名单外的其他标签将会被替换为空字符串

除了Whitelist.relaxed(),还有下面几种预设的可以选择,具体查看:官网Whitelist介绍

除了上面的预设,我们还可以自己添加、删除白名单标签,以及标签中的属性,官网Whitelist介绍中写的很清楚,或者可以直接在源码中查看:org.jsoup.safety.Whitelist类中拥有的方法。

还有需要注意的是,直接使用jsoup处理json字符串的时候,如果json字符串值中有转义的引号例如

"<a class=\"class1\" style=\"width:100px;height:50px;\">测试</a>"其中class以及style的值中的引号都是需要被转义的,那么在被jsoup处理之后的字符串中需要转义的引号将会被再次转义,例如"就会被转义为他的转义符号&quot;,这样就导致处理之后的json字符串在重新转为json对象的时候转换失败,有兴趣大家可以执行main方法看看结果。不过这个问题我们只需要将他添加的转义引号替换掉就可以了。

还有需要注意的是:

Jsoup会对不在白名单的标签进行删除处理, 而且如果标签没有闭合, 会将这个标签一直删除到闭合为止

例如:

String l = "<p文本文本 <span>span内容</span><h1>一级标题</h1>";
System.out.println(clean(l));

由于p标签没有闭合那么结果将会是:(将不匹配的</span>也去掉了)

span内容<h1>一级标题</h1>

注册过滤器:

@Configuration
public class WebConfig {

    /**
     * 功能描述: 注册XssFilter 过滤器。
     * @return org.springframework.boot.web.servlet.FilterRegistrationBean
     * @author cdfan
     * @date 2020/5/25 17:16
     */
    @Bean
  	@ConditionalOnProperty(prefix = "myadmin", name = "xssFilter", havingValue = "true")
    public FilterRegistrationBean xssFilterRegistrationBean() {
        FilterRegistrationBean<XssFilter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new XssFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.setEnabled(true);
        filterRegistrationBean.addUrlPatterns("/*");
//        过滤器初始化参数,
//        Map<String, String> initParameters = new HashMap<>();
//        initParameters.put("excludes", "/favicon.ico,/img/*,/js/*,/css/*");
//        filterRegistrationBean.setInitParameters(initParameters);
        return filterRegistrationBean;
    }

}

这样当我们在通过Controller中获取的request获取参数的时候调用的方法将是我们RequestWrapper中的方法,在方法中就会对参数进行过滤处理,从而避免了跨站脚本攻击,当然如果在不需要处理xss的时候我可以通过配置将把他关掉就行(@ConditionalOnProperty可以控制在什么情况下提供对应的bean)。

项目地址:githubgitee演示环境(账号/密码:admin/123456)

上一篇:独立完成系统开发八:缓存

下一篇:独立完成系统开发十:日志

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值