• React是如何防御XSS的?
  • 发布于 2个月前
  • 236 热度
    0 评论
React这个前端框架大家都很熟悉,但或许很多人不知道它能防御XSS,又或许知道但印象不深。直到最近,我才重新了解到React有这个能力。于是我会想,React是如何防御XSS的?我们在项目里用上了React就不用关注XSS攻击了吗?这篇文章将会逐一回答。

什么是XSS
Cross Site Script,跨站脚本攻击,为了区分层叠式样式表(Cascading Style Sheet,CSS),改叫XSS。一开始这种攻击的演示案例是跨域的,但发展到现在,无论是否跨域,都有可能受到XSS的攻击。

XSS的定义:黑客往用户页面注入恶意脚本,但用户网站没有做任何处理,浏览器也无法判断脚本是否可信,从而让黑客修改了用户页面或者获取了cookies、session tokens等敏感的用户信息。

XSS是一种注入式攻击。

XSS的主要类型
早期XSS分为3种类型:反射型、存储型、DOM Based型。

反射型
服务器直接从用户请求里取出恶意代码返回给用户,主打一个“反射”。

典型攻击步骤:
1.攻击者构造特殊url,其中包含恶意代码;
2.用户请求该url,服务端取出url上的恶意代码,并拼接到html里返回;
3.用户页面执行恶意代码。
场景:
假设我们在某社交网站A上搜索内容,就会跳转以下页面。
https://www.duidaima.com/search?query=searchtext
这个页面的html包含以下内容。
<p>您搜索的内容是:searchtext</p>
如果黑客构造出以下url,并隐藏在邮件里发送给用户。
https://www.duidaima.com?query=<script>alert(1)</script>
当用户点击该url,跳转到社交网站A后,页面会出现弹窗。
<p>您搜索的内容是:<script>alert(1)</script></p>

存储型
服务端从数据库或者消息队列等存储介质中取出恶意代码返回给用户,主打一个“存储”。它跟反射型XSS的区别在于是否有持久化储存。

典型攻击步骤:
1.攻击者将恶意代码提交到服务端,保存到数据库;
2.用户请求页面,服务端从数据库取出恶意代码,拼接html返回给用户;
3.用户页面执行恶意代码。
场景:
比较典型的就是一些论坛上,黑客的评论内容里包含恶意脚本,提交后保存到数据库。当用户访问帖子时,返回的页面包含该脚本,用户受到攻击。

DOM Based型
客户端执行js将恶意代码插入页面,比如使用eval()、innerHTML、document.write()等不安全的API。
典型攻击步骤:
1.攻击者构造特殊url,其中包含恶意代码;
2.用户请求该url,页面js从url中取出恶意代码并执行。
场景:
和上面反射型XSS的场景类似,只不过是由js取出query参数,并插入到页面中。
// https://www.duidaima.com?query=<img src onerror="alert(1)" />
const div = document.createElement('div');
div.innerHTML = decodeURIComponent(getQuery()); // 获取url上的参数
document.body.appendChild(div);
DOM Based XSS又可以分为反射型DOM Based XSS和存储型DOM Based XSS,而反射型XSS和存储型XSS又都可以分为DOM Based的和非DOM Based的,因此前面3种XSS在概念上有重叠部分。所以后来人们按由谁取得恶意代码并插入html中,将XSS分为Client型和Server型两类。

Server型XSS,无论从请求还是数据库中取出恶意代码,都是由服务端将恶意代码组装到html的。
Client型XSS,根本原因是前端js代码的安全漏洞,才导致用户受攻击。

前面说的反射型XSS和存储型XSS属于Server型,DOM Based XSS属于Client型。

XSS常见的有效荷载
<script>alert(1)</script>
<input onfocus="alert(1)" autofocus />
<img src onerror="alert(1)" />
<svg onload="alert(1)"></svg>
<a href="javascript:alert(1)">跳转</a>
总得来说,就是用script标签、href属性、onload等事件回调插入脚本。css的url('javascript:alert(1)')语句在现在的主流浏览器上已经不会造成XSS攻击。

XSS常见的防御手段
转义html
在可能插入<script>的地方对内容进行html转义,可以避免以下XSS攻击:

直接往标签中间插入,比如<div><script>alert(1)</script></div>;
通过属性值提前闭合标签,比如<input type="text" value="insertedValue">这段代码里insertedValue位置处插入"><script>alert(1)</script>,得到<input type="text" value=""><script>alert(1)</script>">这样的html结构。
使用安全的API修改DOM
小心使用.innerHTML、.outerHTML、document.write()等方法,它们很容易带来XSS攻击,使用.innerText、.textContent等方法会更安全。

小心能将字符串作为代码运行的地方
内联事件监听器,onclick、onload、onerror等;
标签属性,比如a标签的href属性;
eval()、setTimeout()、setInterval()等;
CSP
内容安全策略(Content Security Policy),可信白名单机制,通过设置http头部或者配置<meta>开启,禁止不可信来源的内容的加载和执行。

http-only Cookie
XSS攻击一般是为了窃取用户信息。将cookie标记为http-only,避免恶意脚本获取cookie。

React如何防御XSS
主要是上面提到XSS常见防御手段的前两种。

React防御XSS,关键在于jsx的处理上。XSS分为Client型和Server型,React在客户端和服务端对XSS的防御方式又不一样。所以下面会从Client和Server两个方面分析。

Client
我们知道,jsx实际上是React.createElement的语法糖,每一个元素在插入html之前都要先转成下面格式的对象:
{
  $$typeof: Symbol('react.element'),
  type: 'h1',
  key: null,
  props: {
    children: 'Hello, world!',
    className: 'greeting'   
  }
  ...
}
记住这个知识点。下面就来讲React客户端如何防御XSS。React使用.textContent将props.children作为文本插入html。所以下面代码不会有问题。
function App() {
  const [text] = useState('<img src onerror="alert(1)" />');

  // 没问题,img标签以文本形式展示
  return <div>{text}</div>;
}
注意,不应该使用.innerHTML代替.textContent来插入内容。

通过document.getElementById('input').value = xxx或者.setAttribute()设置元素属性一般来说是安全的,不会有前面提到的“提前闭合标签”的问题。
function App() {
  const [text] = useState('"><img src onerror="alert(1)" />');

  // 没问题
  return <input type="text" value={text} />;
}
像下面这样直接在jsx里加入<script>是不行的,它既不会执行,也不会显示。
function App() {
  return (
    <div>
      {/* 无效 */}
      <script>alert(1)</script>
    </div>
  );
}
因为React是这样创建script元素的:
// Create the script via .innerHTML so its "parser-inserted" flag is
// set to true and it does not execute
const div = ownerDocument.createElement('div'); // 这里的ownerDocument指document
div.innerHTML = '<script><' + '/script>';
// This is guaranteed to yield a script element.
const firstChild = div.firstChild;
domElement = div.removeChild(firstChild);
即通过.innerHTML的方式插入<script>使得它不可执行。
使用dangerouslySetInnerHTML属性插入<script>也不行,因为底层也是调用.innerHTML。

Server
主要关注React服务端生成html时做了哪些事情。
转义特殊字符:React对元素属性和内容中的5种特殊字符进行转义,参考以下源码:
function escapeHtml(string) {
 // ...
  const str = '' + string;
  const match = matchHtmlRegExp.exec(str);

  if (!match) {
    return str;
  }

  let escape;
  let html = '';
  let index;
  let lastIndex = 0;

  for (index = match.index; index < str.length; index++) {
    switch (str.charCodeAt(index)) {
      case 34: // "
        escape = '&quot;';
        break;
      case 38: // &
        escape = '&amp;';
        break;
      case 39: // '
        escape = '&#x27;'; // modified from escape-html; used to be '&#39'
        break;
      case 60: // <
        escape = '&lt;';
        break;
      case 62: // >
        escape = '&gt;';
        break;
      default:
        continue;
    }

    if (lastIndex !== index) {
      html += str.substring(lastIndex, index);
    }

    lastIndex = index + 1;
    html += escape;
  }

  return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
}

React不是万能
虽然React在客户端和服务端都有做一些事情来防御XSS,但它依然有漏洞。
dangerouslySetInnerHTML
在一些场景下,我们的确需要直接往页面插入html代码。React为我们提供dangerouslySetInnerHTML属性来实现这个功能,这有可能造成XSS攻击。
function App() {
  // 必须通过__html属性才能注入html代码
  return <div dangerouslySetInnerHTML={{__html: '<img src onerror="alert(1)" />'}} />;
}
虽然属性名dangerouslySetInnerHTML和对象结构{__html: string}都在提醒开发者要谨慎使用,但有些情况还是容易蒙蔽开发者的眼睛。比如,如果我们把后端接口返回的数据直接当作属性赋给元素,而这个数据又包含dangerouslySetInnerHTML属性,就可能造成XSS攻击。这种情况开发者是很难注意到的。
function App() {
  const [data, setData] = useState({});
 useEffect(() => {
    getDataFromServer().then((res) => {
      // 假设res是对象:{dangerouslySetInnerHTML: {__html: '<img src onerror="alert(1)" />'}}
      setData(res);
    });
  }, []);

  // alert(1)
  return <div {...data} />;
}
另外一种隐藏dangerouslySetInnerHTML的方式是给将一个React element对象直接作为children插入。jsx转换过程中每一个元素和它的子元素都会递归地转成React的element对象。所以,我们可以考虑这样手动构造对象,插入到jsx:
function App() {
  const [child] = useState({
    $$typeof: null,
    ref: null,
    type: 'div',
    props: {
      dangerouslySetInnerHTML: {
        __html: '<img src onerror="alert(1)"/>',
      },
    },
  });

  // 没问题
  return <div>{child}</div>
}

但这样会报错:
Objects are not valid as a React child (found: object with keys {$$typeof, ref, type, props}). If you meant to render a collection of children, use an array instead.
原因在于React会校验$$typeof是否为合法值。所以有些人认为这也是React防御XSS的一种手段。诚然,这有一定效果。但因为React使用Symbol.for()而不是Symbol()来生成它的节点类型,我们完全能够构造出合法的$$typeof。比如:
function App() {
  const [child] = useState({
    $$typeof: Symbol.for('react.element'),
    ref: null,
    type: 'div',
    props: {
      dangerouslySetInnerHTML: {
        __html: '<img src onerror="alert(1)"/>',
      },
    },
  });

  // alert(1)
  return <div>{child}</div>
}
这是第二种隐藏dangerouslySetInnerHTML并插入恶意代码的方式。不过,因为Symbol是不可以被Json序列化的,所以我们不用担心将接口返回的数据直接当作children插入jsx会导致XSS攻击。

字符串可以作为代码运行的地方
a标签的href属性就是一个很好的例子,如果我们将后端接口返回的url直接赋给href,就可能造成XSS攻击。
function App() {
  const [href] = useState('javascript:alert(1)');

  return <a href={href}>跳转</a>;
}

小结
总的来说,接口数据用在props和children时,我们要多留一个心眼。

总结
React作为一个著名的前端框架,它在XSS防御上也做了不少事情,比如特殊字符转义、使用安全的API修改DOM、禁止<script>执行。可以说,React在它的领域范围内已经尽可能地帮我们防住了XSS攻击。

但这并不代表我们在React项目开发中可以高枕无忧。在一些可以执行代码或者利用dangerouslySetInnerHTML的地方,我们需要提高警惕,无法确保数据安全时需要做好防范措施。
用户评论