14
由于前段时间业务有接触到富文本编辑器,且编辑器由用户直接使用,所以不可避免需要对其涉及到的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,"&lt;")
    .replace(/>/g,"&gt;")
    .replace(/ /g,"&nbsp;")
    .replace(/\'/g,"&#39;")
    .replace(/\"/g,"&quot;");
}

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代码:

<a href\="javascript:alert('xss')"\>click me</a\>

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

<p onclick="alert('xss')">this is a text</p>
<img src="xxx.com/test.jpg" onerror="alert('xss')"/>

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

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

<div style="position:absolute;top:0;left:0;width:2000px;height:2000px;z-index:9999;">
点击劫持的元素,阻止页面其他操作</div>

<div style="background:url(javacript:alert('xss'))">
借助style标签注入脚本,大部分xss过滤库会帮我们过滤这部分脚本</div>

<div style="background:url(//xxx.com/H图.jpg)">
偷偷加载其他网站的小H图,绕过过滤和审核</div>

可见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 定义<audio>、 <video> 等引用资源加载策略
object-src 定义 <applet>、 <embed>、 <object> 等引用资源加载策略
script-src 定义JS加载策略
style- src 定义CSS加载策略

对应的指令value可以设置为:(图片引自:https://www.jianshu.com/p/4bad03d89c04
image.png
相信聪明的你看了这个规则一看就懂了,接下来,以meta标签设置CSP为例,我们可以如下设置,以此来限制对白名单外资源加载:

<meta http-equiv="Content-Security-Policy" content=
    "script-src 'self' *.qq.com *.cdn-go.cn; 
    img-src 'self' *.cdn-go.cn *.gtimg.cn data:;
    style-src 'unsafe-inline' *.cdn-go.cn; 
    media-src 'none'; 
    child-src *.qq.com">

5、其他HTTP安全响应头设置

除了CSP这个最常用最强势的限制规则外,还有其他的HTTP安全响应头,如果心情好的话,可以统统整上(滑稽):

1)、X-Frame-Options:X-Frame-Options HTTP 响应头是用来给浏览器指示允许一个页面可否在 <frame>, <iframe>或者 <object> 中展现的标记。网站可以使用此功能,来确保自己网站的内容没有被嵌到别人的网站中去,也从而避免了点击劫持 (clickjacking) 的攻击。
X-Frame-Options 有三个值:

DENY
表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。
SAMEORIGIN
表示该页面可以在相同域名页面的 frame 中展示。
ALLOW-FROM uri
表示该页面可以在指定来源的 frame 中展示。

2)、X-Content-Type-Options:X-Content-Type-Options 响应首部相当于一个提示标志,被服务器用来提示客户端一定要遵循在 Content-Type 首部中对 MIME 类型 的设定,而不能对其进行修改。这就禁用了客户端的 MIME 类型嗅探行为,(类型嗅探指的是浏览器在加载资源时,自动嗅探资源类型过程中,导致执行了注入脚本),换句话说,也就是意味着网站管理员确定自己的设置没有问题。

3)、X-XSS-Protection:当检测到跨站脚本攻击 (XSS)时,浏览器将停止加载页面。

然而这个响应头由于只有IE、谷歌等少部分浏览器支持,且不同浏览器的实现不一致,存在错误过滤行为,甚至可能导致带额外的漏洞,所以在实际情况中较少使用。(为避免错误过滤,部分门户网站会将其值设置为0)

X-XSS-Protection: 0
X-XSS-Protection: 1
X-XSS-Protection: 1; mode=block
X-XSS-Protection: 1; report=<reporting-uri>
0禁止XSS过滤。
1启用XSS过滤(通常浏览器是默认的)。 如果检测到跨站脚本攻击,浏览器将清除页面(删除不安全的部分)。
1;mode=block启用XSS过滤。 如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载。
1; report=<reporting-URI> (Chromium only)启用XSS过滤。 如果检测到跨站脚本攻击,浏览器将清除页面并使用CSP report-uri指令的功能发送违规报告。

以推特网站为例,其设置的响应头如下:
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 0

三、总结

本文主要总结归纳了实践过程中针对XSS漏洞的防护措施,首先针对用户输入源和不可信数据源需要我们做好必要的过滤和校验,其次,通过CSP安全策略和其他HTTP响应头的设置等进一步确保安全性。

只有综合多种手段和方法才能确保自己的网站更加安全,如果不安全,那么就删git仓库跑路。

谢谢观看~


曾培森
1.1k 声望875 粉丝

学海无涯皮蛋瘦肉粥