前端安全之防范XSS实战小结

由于前段时间业务有接触到富文本编辑器,且编辑器由用户直接使用,所以不可避免需要对其涉及到的XSS防护有所了解,因此对XSS防护做一个实战小结。

前言

XSS大部分前端coder都不会陌生,全称:跨站脚本漏洞(Cross Site Scripting,简写作XSS)是Web应用程序在将数据输出或者展示到网页的时候存在问题,导致攻击者可以将对网站的正常功能造成影响甚至窃取或篡改用户个人信息,其诱发的主要原因是没有针对用户输入来源的数据以及不可信数据源的过滤。本文将针对XSS进行简单的归类总结,并且提供相关的实战处理手段。

一、XSS 类型划分

对于常见的XSS攻击方式,大部分人再熟悉不过了,这里再帮大家复习一下。

1、反射性XSS:常见情况是攻击者通过构造一个恶意链接的形式,诱导用户传播和打开,由于链接内所携带的参数会回显于页面中或作为页面的处理数据源,最终造成XSS攻击。

2、存储型XSS:与前者不同,存储型XSS是持久化的XSS攻击方式,通过用户输入个人信息或者发表文章的方式将恶意代码存储于服务器端,当其他用户再次访问页面时触发,造成XSS攻击。

3、DOM based型XSS:同样也是利用对数据源不可靠,缺乏过滤,由于开发过程中,不可避免部分数据需要回填到DOM中,例如href、src、data-id等标签属性,而通过构造特殊的字符串闭合原有的DOM标签,在其中注入script标签的方式造成攻击。

二、XSS防范手段

针对XSS的防范,需要开发者养成意识,针对用户输入源的数据进行过滤,针对页面不可信任的数据源也要做好过滤,类似于响应头中的字段、url中的参数、refer等字段都是不可信的,并且结合其他手段和方法,让页面更加安全可控。

1、Htmlencode转义特殊字符

大部分的论坛或者博客平台,注册账号都会允许用户填写个人信息,包括昵称、邮箱和个性签名等,此类文本信息属于非富文本类型,最常见的方法就是对尖括号标签转义成实体字符存储,由于是非富文本信息,并且以标签内容的形式展示,如果不使用框架渲染,直接原生JS通过docoment.getElementById('xxx').innerText = 'your name'展示回页面中即可。

常见的处理方式如下:

const htmlEncode = function (handleString){
    return handleString
    .replace(/&/g,"&")
    .replace(//g,">")
    .replace(/ /g," ")
    .replace(/\'/g,"'")
    .replace(/\"/g,""");
}

2、引入XSS库针对用户输入源过滤,设置标签白名单

前面说明了在非富文本的情况下较为快捷方便的处理方法,然而在大部分论坛中发帖发文章都是富文本编辑的形式,即最后回显于页面中的是一段HTML内容,不能单纯以文本形式处理。

大部分的富文本编辑器原理,都是提供一个具备contenteditable属性的dom元素,让用户对一段富文本进行编辑,其本质是对一段html进行处理,新增或删除样式等,最后通过回传富文本框中html的方式提供给开发者,意味着我们要允许用户填充一段html于我们的页面中。而获取到的html字符串我们不能直接进行简单的标签替换,否则会导致原有的样式丢失,最终展示在页面中的也不再是一篇排版精致的文章,因此我们要另寻他路。

首先需要明确的是,无论是这份来自于富文本编辑器的html,还是来自于最终用户发起请求所获取到的html,都是不可信的,意味着在前端进行过滤是没有任何实际意义和价值的,因为攻击者可以轻易的伪造请求绕过限制,所以我们需要在我们的服务器端针对这段html进行过滤处理。针对html的标签白名单过滤,不同的语言有不同的库实现,这里主要介绍nodejs中常用的标签过滤库,nodejs中常用的库主要是xss和xss-filter,下面以xss库的使用为例:

import * as xss from 'xss';
function handleXss(content: Content) {
  // 设置HTML过滤器的白名单
  const options = {
    whiteList: {
      p: ['class', 'style'],
      em: ['class', 'style'],
      strong: ['class', 'style'],
      br: ['class', 'style'],
      u: ['class', 'style'],
      s: ['class', 'style'],
      blockquote: ['class', 'style'],
      li: ['class', 'style'],
      ol: ['class', 'style'],
      ul: ['class', 'style'],
      h1: ['class', 'style'],
      h2: ['class', 'style'],
      h3: ['class', 'style'],
      h4: ['class', 'style'],
      h5: ['class', 'style'],
      h6: ['class', 'style'],
      span: ['class', 'style'],
      div: ['class', 'style'],
      img: ['src', 'class', 'style', 'width'],
    },
  }; 
  // 自定义规则
  const myxss = new xss.FilterXSS(options);
  // 直接调用 myxss.process() 即可
  content.content = myxss.process(content.content);
  return content;
}

通过限定白名单,仅允许常见的文本展示标签以及图片img标签进入白名单,这部分会再过滤后被保留,并且标签内的class和style属性也会被保留;其他属性和诸如script、iframe等标签都会被直接过滤掉。 

这里为何要严格限定标签的以及属性的原因,例如一个a标签可以通过href属性注入一段类似这样的js代码:

click me

更有甚者会对注入的字符进行大小写字符转换,或转义成等效字符,或插入空格或tab,但脚本仍然能正常运行,造成攻击。

this is a text

其次,普通的标签可以直接通过绑定onclick的方式攻击,即便是img、video等资源加载标签,也可以通过onload、onerror等事件注入脚本,可见针对标签内属性的过滤也是不可或缺的。

然而本以为这样已足够,但是即使是只开放了class和style属性开放了,也是不安全的,关键在于style属性,如果任由用户自定义的话,可以通过style属性实现:点击劫持(将元素铺满整个界面)、加载外域图片、脚本注入甚至可以给文章设置一些花里胡哨的动画:

点击劫持的元素,阻止页面其他操作
借助style标签注入脚本,大部分xss过滤库会帮我们过滤这部分脚本
偷偷加载其他网站的小H图,绕过过滤和审核

可见style属性也不容忽视,因此我们需要在option参数中额外为style属性设置白名单,确保style属性安全可控:

...
   css: {
      whiteList: {
        color: true,
        'background-color': true,
      },
    },
...

其次为了确保不加载非本域名下的图片资源,我们也可以再这一层做一些针对img标签的过滤:

    ...
    stripIgnoreTag: true,
    onTagAttr: (tag:string, name:string, value:string, isWhiteAttr:boolean) => {
      // 判断img下的src属性 如果非本域名下 返回空
      if (isWhiteAttr && tag === 'img' && name === 'src' && !checkLegal(value)) 
      {
        return '#';
      }
    },
    ...

3、cookie设置HttpOnly,配合token或验证码防范

针对信息源的过滤,针对不可信数据源的过滤,已经能达到初步的效果,但这远远不够,毕竟没有绝对的安全。

由于大部分攻击者会想要获取到用户的cookie去做别的坏事,所以我们需要在http的响应头set-cookie时设置httpOnly,让浏览器知道不能通过document.cookie的方式获取到cookie内容。

app.get('/', (req, res) => {
    if(req.cookies.isVisit) {
        console.log(req.cookies)
        res.send('欢迎再次光临')
    } else {
        res.cookie('isVisit', 1, {maxAge: 3600 * 1000, httpOnly: true}) 
        res.send('欢迎初次光临')
    }
})

虽然避免了攻击者直接获取到cookie,但是攻击者仍然可以页面内发起别的请求,直接篡改用户的信息,因此需要我们配合token或者验证码的形式,防止攻击者直接通过脚本的方式篡改用户个人信息。而这个token类似于CSRF中我们所需要的token,不过如果攻击者仔细研究了代码,并且知道的token在页面中的来源,也是可以直接获取到token的,因此,相比之下验证码安全性更高。

4、设置CSP安全策略

除了针对数据源的严格过滤以外,CSP安全策略的限制也是主要的XSS防范手段之一,通过在页面中设置允许加载的资源的来源,来严格限制页面可加载的脚本以及图片等资源,防止外部的脚本攻击后注入其他脚本以及内容。

CSP,内容安全策略,是一种基于内容的声明式网络应用程序机制,对缓解内容注入漏洞的危害非常有效。通过一系列指令告诉客户端(如浏览器)被保护资源(如页面)内只允许加载和执行指令集中限定的内容,类似白名单机制,不满足限定条件的资源和内容将被客户端阻断或不被执行。可以通过两种方式设置CSP,一种是meta标签,一种是HTTP响应头Content-Security-Policy:

指令及其说明:
default-src 定义资源默认加载策略
connect-src 定义Ajax、 WebSocket等加载策略
font-src 定义Font 加载策略
frame- src 定义Frame加载策略
img-src 定义图片加载策略
media- src 定义