前端安全之 XSS 攻击 | 七日打卡

790 阅读5分钟

什么是 XSS

概念

跨站脚本攻击(Cross-site scriptingXSS)是一种在 Web 应用中的安全漏洞,它允许恶意 Web 用户将代码植入到提供给其它用户使用的页面中。

一旦 Web 应用没有进行安全验证,这些攻击很容易成功。裸奔的恶意代码,可想而知是非常危险的。这些脚本可以任意读取 cookie,session tokens,或者其它敏感的网站信息,或者让恶意脚本重写 HTML 内容 。

分类

一般来说,XSS 攻击可以分为3类:存储型(持久型)反射型(非持久型)、和 DOM 型DOM 型 跟前两种 XSS 的区别的区别是:在 DOM 型 XSS 攻击中,取出和执行恶意代码都由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。

存储型 XSS

存储型,意思就是恶意的代码片段被永久地存储在被攻击网站的服务器上。当用户进入网页,浏览器请求资源的同时,恶意脚本从服务器上传回并执行。

反射型 XSS

反射型 XSS 通常在用户被诱导点击点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站的时候发生的,响应的内容可能被插入到scriptastyle等标签中,由于浏览器认为这个响应来自"可信任"的服务器,所以会执行这段脚本。

DOM 型 XSS

DOM 型和反射型 XSS 的攻击原理相似,但是通常不需要浏览器响应来完成攻击,一般是用户执行了某个操作,导致客户端代码被包含进了页面并且意外执行,从而导致 DOM 环境被恶意修改。

常见危害

  • 通过 document.cookie 窃取 cookie 信息
  • 使用 js 或者 css 破坏页面的正常结构与样式,使得用户无法正常访问
  • dos攻击:利用合理的客户端请求来占用过多的服务器资源,从而使合法用户无法得到服务器响应

常见场景

// This WILL run
div.innerHTML = '<script deferred>alert("XSS Attack");</script>';
// This will, too
div.innerHTML = '<img src=x onerror="alert(\'XSS Attack\')">';
<img src=javascript:alert("XSS") />

前端的 XSS 防御姿势

输入安全

前端可以对提交的内容进行过滤、转义处理,比如表示 html标记的 <> 等符号。但是前端并不能完全解决这个问题,因为前端校验可以被绕过,模拟直接请求,所以后台也要有相应的安全策略。

function encode(str) {    
  if (!str || str.length === 0) return '';
  str = str.replace(/>/gm, '&gt;');
  str = str.replace(/</gm, '&lt;');
  str = str.replace(/"/gm, '&quot;');
  str = str.replace(/'/gm, '&apos;');    
  return str;
}

插入安全

客户端 XSS,也就是 DOM 型 XSS 攻击,通常由 JavaScript 执行了不安全的操作导致的。比如 innerHTMLdocument.write()location.href 等在页面中插入代码的操作产生的。在不能确保内容安全的情况下,我们要采用更加安全的替代方案。

textContent 替代 innerHtml

对于不包含标签的插入的内容,我们使用textContent 插入,这个时候内容会被转义,浏览器将内容识别为文本,因此插入的脚本不能执行

var sanitizeHTML = function (str) {
	var temp = document.createElement('div');
	temp.textContent = str;
	return temp.innerHTML;
};

这个时候,插入操作变成:

div.innerHTML = '<h1>' + sanitizeHTML('<img src=x onerror="alert(\'XSS Attack\')">') + '</h1>';

DOMPurify

对于需要直接插入 HTML 的情况,我们可以使用第三方库 DOMPurifyjsxss等来处理。主要原理是,通过移除所有非安全的 HTML 内容(标签),从而得到一个“纯净”的 HTML。

const DOMPurify = require('dompurify')(window);
// Sanitize the review
return (<p dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(review)}}></p>);

资源安全

HttpOnly

对于不需要对 JavaScript 可用,只是用于持久化服务器端会话的 Cookie,我们通过设置 HttpOnly 标识,防止恶意脚本通过 document.cookie 访问到用户隐私数据。

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; HttpOnly

CSP

内容安全策略(Content Security PolicyCSP),可以通过约定一系列的安全策略,指定可信的内容来源。一个兼容 CSP 的浏览器将只会执行加载与白名单域名的源文件的脚本,忽略那些其他的脚本(包括内联脚本和事件操控HTML属性)。

  • 使用
// 禁止外域脚本
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' ssl.google-analytics.com;">
  • 其他例子
//所有内容来自站点的同一个源(不包括子域名)
Content-Security-Policy: default-src 'self'
// 允许信任的域名
Content-Security-Policy: default-src 'self' *.trusted.com
//所有内容都要通过SSL方式获取
Content-Security-Policy: default-src https://onlinebanking.jumbobank.com

SRI

子资源完整性(Subresource IntegritySRI),是允许浏览器检查其获得的资源(例如从 CDN 获得的)是否被篡改的一项安全特性,可用来解决由于 CDN 资源被污染(恶意注入/替换)而导致的 XSS 漏洞。

  • integrity: 文件指纹。密码散列可以唯一标识一个数据块,任何两个文件的密码散列均不相同。浏览器在下载脚本后会计算它的散列,然后将得出的值与 integrity 提供的值进行比较。如果不匹配,则说明目标脚本被篡改,浏览器将不使用它。

我们在引入一些资源的时候,经常看到这个属性的身影:

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>

可以结合 CSP 对站点进行配置:

# 规定了所有脚本都要有 integrity 属性,且通过验证才能被加载
Content-Security-Policy: require-sri-for script;
# 规定了所有样式表都要有 integrity 属性,且通过验证才能被加载
Content-Security-Policy: require-sri-for style;

React 中的 XSS 防御

默认转义

在 React 中,默认会对内容文本进入转义处理,减少我们自己处理的负担

<div>{ text }</div>

JSX

我们知道,JSX 在编译阶段实际是调用了 React.createElement() 函数来创建实际的标签,它会告诉 React 接下来要渲染的内容和一个唯一标识 $$typeof,React 会检测 element.$$typeof,如果标识缺失或者无效,会拒绝处理该元素。

{
  type: 'div',
  props: {
    children: 'text',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element')
}

参考资料