关于XSS攻击及其防御

XSS

  • XSS 是 Cross Site Scripting 跨站脚本攻击的意思,X是英文Cross的简称
  • 什么叫做跨站,就是非自己网站,如果本网站运行了来自别的网站的东西就叫做XSS
  • 举个例子,如下是前端页面代码
    
    if from
        span -- 欢迎来自!{from}的朋友
    
  • 下面是后端逻辑代码
    // from 来源于get参数
    ctx.render('index', {posts, comments, from:ctx.query.from || ''});
    
  • 现在,如果我们在页面url上加上如下代码 ?from=
  • 可以看到页面弹出1,这就是一段XSS攻击代码,为什么是跨站攻击呢?我们可以做如下变通
    • ?from=
  • 这种跨站脚本可以执行任何形式的攻击,其攻击原理是:程序+数据=结果
  • 当我们的数据中包含一部分程序的时候,原来我们程序的逻辑就会被改变了
  • 页面中的脚本可以干什么,XSS的脚本就可以看什么,比如
    • 获取页面数据
    • 获取Cookie
    • 劫持前端逻辑
    • 发送请求
    • 偷取网站任意数据资料,密码,登录态
    • 欺骗用户
  • 可见XSS是一种非常危险的前端攻击手段
  • 常见的XSS攻击案例
    • 站酷的搜索框
    • QQ空间的富文本编辑器,script脚本侧漏
    • 电商表单提交脚本,在后台页面运行,获取后台地址和登录态

XSS攻击的分类

  • XSS攻击的种类很多,尤其是变种很多,我们可以按照攻击代码的来源分为两种
  • 1 ) 反射型:url参数直接注入, 危害略小
    • 攻击者一般会把这种参数变换,比如做一个短网址,在dwz.cn中变换为短网址
    • 一般受害者可能会忽略误点从而导致受害
    • 反射型攻击可以和其他攻击结合进而自动传播
  • 2 ) 存储型:存储到DB后读取时注入,攻击代码来自网站数据,危害较大
    • 一般通过富文本编辑器将脚本注入数据库中
    • 当用户浏览到注入脚本数据的页面时,就会中招

XSS攻击注入点

  • HTML节点内容:如果内容是动态生成了,包含用户输入,那么这个输入中可能包含脚本,进而导致XSS攻击
    • 比如程序是这样的:
      #{content}
      , 但是可能会变成这样:
    • 主要是存储型XSS攻击,内容提交后注入
  • HTML属性:某个HTML节点的某一个属性是由用户输入构成的,用户输入数据有可能包含脚本或者越出属性本身的范围导致XSS攻击
    • 比如原先是这样的,结果却成了这样
    • 具体程序程序比如前端是这样做的
      
      if avatarId
          <img src="/image/!{avatarId}" />
      
    • 后端是这样的
      // 同样是通过某种参数来引用的
      ctx.render('index', {avatarId:ctx.query.avatarId || ''});
      
    • 只需要在url中这样访问即可:?avatarId=1" onerror="alert(1) 此时产生一个onerror的新属性
    • 这时候就会执行其中的脚本,这明显是一种反射型攻击,其原理是通过冒号提前关闭原属性,并产生一个新的属性
  • js代码:存在由后台注入的变量,或里面包含用户输入的信息,有可能用户输入信息会改变js代码的逻辑导致XSS攻击
    • 原本逻辑是这样的var data = "#{data}";, 结果却成了 var data = "hello";alert(1);"";
    • 这里的data来自服务器的数据,这个数据很可能是之前用户输入的,也就是用户提前将这个变量进行了关闭
    • 这时候原本的一句代码变成了3句代码,这种可以是反射型攻击,也可能是存储型攻击
  • 富文本:富文本是一大段的HTML标签,我们要保留html格式也要小心其中可能包含XSS攻击的代码
    • 以QQ邮箱为例,可以设置任意的格式,本质上是一段复杂的html
    • 富文本会保留HTML, 但是HTML具有XSS攻击的风险

XSS常见的防御方案

1 ) 浏览器自带防御

  • 浏览器自带防御:参数出现在HTML内容或属性中,浏览器会自行拦截并提示: ERR_BLOCKED_BY_XSS_AUDITOR
    • 如果发现谷歌浏览器等现代浏览器没有进行拦截,可能是我们设置了取消拦截
    • ctx.set('X-XSS-Protection', 0) 具体有三种参数
    • 第一种是 0 是关闭保护
    • 第二种是 1 是默认开启
    • 第三种是1后面再加一个url参数,谷歌浏览器会向这个url发送一个通知
    • 它防御范围非常有限,即反射型XSS攻击,而且只防御HTML相关的注入(内容或属性)
    • 而在js中注入的参数不会被拦截, 如 url: ?from=";alert(document.cookie);", js脚本:var from = "!{from}";
    • 而且并不是所有浏览器都支持

2 ) 转义HTML节点内容和属性

  • 通过程序处理HTML节点内容
    • 对HTML内容进行转义,只需要转义<<>> 即左右尖括号
    • 转义的时机可以有几个选择:1 )进入数据库之前; 2 )显示的时候进行转义(简单起见使用这种)
    • 我们只需要定义这种转义函数, 在输出之前进行转义
      var escapeHtml = function(str) {
          if(!str) return '';
          str = str.replace(/</g, '<');
          str = str.replace(/>/g, '>');
          return str;
      }
      ctx.render('index', {from: escapeHtml(ctx.query.from) || ''});
      
  • 通过程序处理HTML节点属性
    • 对应的策略是转义引号,即:" &quto;
    • 也是定义一种转义函数
      var escapeHtmlProperty = function(str) {
          if(!str) return '';
          // 处理单引号和双引号
          str = str.replace(/"/g, '&quto;');
          str = str.replace(/'/g, ''');
          // 属性可以没有引号,处理没有引号的情况,即空格
          str = str.replace(/ /g, ' ');
          return str;
      }
      ctx.render('index', {avatarId: escapeHtmlProperty(ctx.query.avatarId) || ''});
      
  • 其实这两种HTML的过滤,过滤内容和属性是没有冲突的,我们可以把两者合并,如下, 有两种方式
    • 方式1
      var escapeHtml = function(str) {
          if(!str) return '';
          str = str.replace(/</g, '<');
          str = str.replace(/>/g, '>');
          str = str.replace(/"/g, '&quto;');
          str = str.replace(/'/g, ''');
          str = str.replace(/ /g, ' ');
          return str;
      }
      ctx.render('index', {from: escapeHtml(ctx.query.from) || '', avatarId: escapeHtml(ctx.query.avatarId) || ''});
      
    • 方式2
    // type 0 过滤内容,type 1过滤属性
    var escapeHtml = function(str, type) {
        if(!str) return '';
        if(type === 0) {
            str = str.replace(/</g, '<');
            str = str.replace(/>/g, '>');
        }
        if(type === 1) {
            str = str.replace(/"/g, '&quto;');
            str = str.replace(/'/g, ''');
            str = str.replace(/ /g, ' ');
        }
        return str;
    }
    ctx.render('index', {from: escapeHtml(ctx.query.from, 0) || '', avatarId: escapeHtml(ctx.query.avatarId, 1) || ''});
    
    • 只是不同的封装方式而已
  • 这样其实还是存在问题的
    • 在html中多个连续空格,在渲染时候只会产生一个空格
    • 如果将空格全部转换为html实体的时候,页面上的显示可能会有问题
    • 一般来说,我们对空格不做任何转义,这样的话,我们要约定:属性一定要带上引号
    • 按照html的规范,在html5之前,&也是需要进行转义的,在html5之后不需要了
    • 按照惯例,一般还是会做一下转义,这样我们的转义程序,就变成了
      var escapeHtml = function(str) {
          if(!str) return '';
          str = str.replace(/&/g, '&'); // 这个只能放在最前面,否则会转义下面被转义过的含有&的转义字符
          str = str.replace(/</g, '<');
          str = str.replace(/>/g, '>');
          str = str.replace(/"/g, '&quto;');
          str = str.replace(/'/g, ''');
          return str;
      }
      

3 ) 转义js代码

  • 在js中会插入来自后台或用户直接输入的数据,这个数据可能突破我们的引号边界添加新的脚本语句
  • 就像上面案例url中:?from=sina";alert(1);" 这个alert(1)会被执行
  • 我们初步的解决方案将数据中的引号转义,如果用到上面的escapeHtml方法会有问题
  • 就会把这个变量变成一串含有转义字符的东西,如sina&quto;;alert(1)&quto;
  • 其实这个在js中对变量来说是不对的,即使仅仅是一个字符串,也是很难看的
  • 这里包含html实体,而js并不能解析html实体,其实转义的html实体它不应该出现在js变量中
  • 我们进一步考虑将js用到的变量和html中的变量进行区分处理
  • 如果用处理html的方法来处理一般数据,可能就会在数据中
    var escapeJs = function(str) {
        if(!str) return '';
        str = str.replace(/"/g, '\\"'); // 此处转义: 将 " 转义为 \"
        return str;
    }
    
    ctx.render('index', {from: escapeJs(ctx.query.from) || ''});
    
  • 通过这种方式,我们就可以得到这样一串js变量sina";alert(1);" 也就是原汁原味的东西,但不会受到攻击
  • 但是,如果访问的url是这样的:?from=sina\";alert(1);" 就会被转义成这样:sina\\";alert(1);\""
  • 此时解析上来说,添加的一个\,会继续转义被转义成的\", 其实转义的只是前面的\, 此时"还是被添加进去了
  • 这时候sina\"其实在变量的概念中就已经闭合了,剩下的;alert(1);"会存在语法错误
  • 报错:Uncaught SyntaxError: Invalid or unexpected token
  • 如果我们输入的url是这样的,?from=sina\";alert(1);//, 后面的//被当成注释,屏蔽了后面默认闭合引号产生语法错误的源头
  • 此时,还是会执行alert(1),依然存在XSS攻击的可能,其原因是输入的\在页面渲染的时候被当成了转义符
  • 这时候,我们可以把\也处理了,如下重写escapeJs函数
    var escapeJs = function(str) {
        if(!str) return '';
        str = str.replace(/\\/g, '\\\\'); // 注意这里的4个\,2个为转义一个,即:\本身是个转义符,表达\本身,需要使用\\
        str = str.replace(/"/g, '\\"'); // 此处转义: 将 " 转义为 \"
        return str;
    }
    
  • 此时就不会报错,或者被攻击了,但是这样也不彻底,因为我们的字符串可能被单引号包裹
    var escapeJs = function(str) {
        if(!str) return '';
        str = str.replace(/\\/g, '\\\\'); // 注意这里的4个\,2个为转义一个,即:\本身是个转义符,表达\本身,需要使用\\
        str = str.replace(/"/g, '\\"'); // 此处转义: 将 " 转义为 \"
        str = str.replace(/'/g, "\\'"); // 此处转义: 将 " 转义为 \"
        return str;
    }
    
  • 如果再处理了单引号,会不会有其他情况呢?也不排除有其他情况,最保险的办法是对我们的数据进行json转义
  • 所以,我们不通过escapeJs这个函数了,而是通过JSON.stringify来处理即可,即
  • ctx.render('index', {from: JSON.stringify(ctx.query.from) || ''});, 这是最好的处理方式

4 ) 转义富文本

  • 所谓富文本就是一大段的HTML,这段HTML可能会包含非常多的格式,我们需要保留这些格式
  • 没办法把全部HTML转义,同样面临着XSS攻击, 一般的思路是做过滤,过滤又有两种
    • 1 ) 按照黑名单进行过滤,如script标签,onerror属性等去除它们
    • 相对简单,但稍不留神就会留下漏洞
    • 2 ) 按照白名单进行过滤,如只允许部分标签和属性
    • 实现较为麻烦,需要将HTML完全解析成数据结构,对其进行过滤,再重组成HTML
  • 通过使用黑名单来过滤,我们可以提供下面一个过滤函数,针对输入的内容进行过滤
    var xssFilter = function(html) {
        if(!html) return '';
        html = html.replace(/<\s*\/?script\s*>/g, '');
        html = html.replace(/javascript:[^'"]*/g, '');
        html = html.replace(/onerror\s*=\s*['"]?[^'"]*['"]?/g, '');
        // ...继续处理其他 svg, object等可以运行脚本,稍不留神少一个就会造成漏洞隐患
        // 所以并不推荐这种
        return html;
    }
    
  • 通过白名单来过滤,保留部分标签和属性,首先需要把一大段的HTML解析成树状结构,Dom树
  • 这个和浏览器解析HTML过程是类似的,针对这颗树,一个一个的遍历,去除不允许的,保留允许的
  • 我们可以通过一个cheerio库来解析html, 就是爬虫中使用到的,返回出一个类似Dom的结构
  • 其特色是使用JQuery的API,上手很快,可以在npmjs官网找到
  • 一般而言,我们在入库的时候处理富文本,这样比查询渲染页面时性能会更高
  • 具体用法如下,我们提供一个xssFilter函数和白名单,对富文本数据进行过滤
    var xssFilter = function(html) {
        if(!html) return '';
        var cheerio = require('cheerio');
        var $ = cheerio.load(html);
        // 白名单
        var whiteList = {
            'img': ['src'],
            'font': ['color', 'size'],
            'a': ['href']
        };
        $('*').each(function(index, ele) {
            if(!whiteList[ele.name]) {
                // 这里一般直接移除
                $(ele).remove();
                // 如果想要保留script中的内容,也可以在remove之前保留其内容
                return true; // true 相当于 continue,return false 相当于 break;
            }
            // 在白名单中
            for(var attr in ele.attribs) {
                if(whiteList[ele.name].indexOf(attr) === -1) {
                    $(ele).attr(attr, null); // 过滤其他不允许属性
                }
            }
            return $.html();
        })
        return html;
    }
    
  • 白名单是一个比较好的方案,但是其设计比较复杂,如果定的不好,可能会影响业务
  • 一般推荐白名单库,也就是别人写好的XSS过滤库,比如github上的js-xss库
  • js-xss 地址:https://github.com/leizongmin/js-xss
  • 我们可以直接这样使用
    var xssFilter = function(html) {
        if(!html) return '';
        var xss = require('xss');
        var ret = xss(html, {
            // 也可以自行指定
            whiteList: {
                img: ['src'],
                a: ['href'],
                font: ['size'],
            },
            onIgnoreTag() {
                return '';
            }
        });
        return ret;
    }
    
  • 使用第三方库可能会有一些意料之外的局限,但是安全一般是可以保障的
  • 如果简便开发建议选择第三方库,如果要高度定制,就要自己来写白名单

PHP防御XSS

1 ) 内置函数转义

  • 演示在一个简单的表单提交中,后端php混编html代码
    
    header('X-Xss-Protection: 0');
    if(strtolower($_SERVER['REQUEST_METHOD']) === 'post') {
        $content = $_POST['CONTENT'];
    }
    ?>
    <div><?php echo $content;?></div>
    <?php } ?>
    
    <form method="post">
        <textarea name="content">hello</textarea>
        <button type="submit">提交</button>
    </form>
    
  • 上面这段代码会存在XSS攻击,我们使用内置函数strip_tags来处理
  • strip_tags会把输入的HTML标签全部过滤掉,比如script,div,p…等标签
    $content = $_POST['CONTENT'];
    $content = strip_tags($content); // 添加这一行即可
    
  • 可以看到,这个内置函数无法解决富文本数据问题,因为标签全部被干掉了
  • 但是如果我们想要保留我们写下的script等标签,可以通过转义& < > ' " 这5个字符来实现
  • 有一个内置函数叫htmlspecialchars可以解决,默认不转义',可以加个ENT_QUOTES参数来解决,如下
    $content = $_POST['CONTENT'];
    // 转义 & < > ' "
    $content = htmlspecialchars($content, ENT_QUOTES); // 添加这一行即可
    

2 ) DOM解析白名单

  • 这是应对富文本攻击非常必要的方法,在PHP中自带DOMDocument这样一个类
  • 这个类把HTML字符串解析成DOM对象,就类似nodejs中的cheerio库一样
  • 这个类中含有操作DOM的各类属性和方法,可以根据这些DOM对象一一遍历处理即可
  • 如果实际操作的话还是比较麻烦的,在生产环境中这样做的也比较少
  • 实际操作中,还是采用phpQuery这样的第三方操作DOM的库

3 ) 第三方库

  • 在php中有很多这样的库来做XSS过滤,在github上通过关键词php xss即可搜索到很多
  • 这里有一个比较典型的库:HTML PURIFIER
  • 下载后实际使用举例
    
    header('X-Xss-Protection: 0');
    require_once './library/HTMLPurifier.auto.php'; // 添加这一行
    if(strtolower($_SERVER['REQUEST_METHOD']) === 'post') {
        $content = $_POST['CONTENT'];
        $purifier = new HTMLPurifier(); // 添加这一行
        $content = $purifier->purify($content); // 添加这一行
    }
    ?>
    <div><?php echo $content;?></div>
    <?php } ?>
    
    <form method="post">
        <textarea name="content">hello</textarea>
        <button type="submit">提交</button>
    </form>
    

4 ) CSP

  • 具体代码如下,参考
    
    header('X-Xss-Protection: 0');
    header("Content-Security-Policy: script-src 'self'"); // 只添加这一行
    if(strtolower($_SERVER['REQUEST_METHOD']) === 'post') {
        $content = $_POST['CONTENT'];
        $purifier = new HTMLPurifier();
        $content = $purifier->purity($content);
    }
    ?>
    <div><?php echo $content;?></div>
    <?php } ?>
    
    <form method="post">
        <textarea name="content">hello</textarea>
        <button type="submit">提交</button>
    </form>
    

你可能感兴趣的:(Web,xss,前端,web安全)