Основные проблемы безопасности при использовании ReactJS
December 11, 2017
React — популярная javascript-библиотека для создания UI.
В данной статье рассмотрим основные уязвимости, которые могут совершать разработчики при создании React-приложений. В React реализована защита по умолчанию от уязвимостей типа HTML-injection, так что разработчики зачастую “забывают” о других возможностях проведения XSS-атак. Также рассмотрим уязвимости типа “CSS injection” в CSS-in-JS библиотеках и способы их эксплуатации.
Компоненты, свойства и элементы
Компоненты позволяют разделить UI на независимые, повторно используемые части и работать с каждой из них отдельно.
Концептуально, компоненты подобны JavaScript функциям. Они принимают произвольные данные (называемые “props”) и возвращают React-элементы, описывающие что должно появиться на экране. Базовый компонент выглядит следующим образом:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}Обратите внимание на синтаксис в операторе return: это JSX, расширение синтаксиса JavaScript, разработанное командой ReactJS. Этот формат используется для упрощения написания компонентов ReactJS. На этапе сборки-препроцессинга JSX транслируется в обычный JavaScript код. Следующие два примера идентичны:
// JSX
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
// Transpliled to createElement() call
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);Новые элементы создаются с помощью функции createElement():
React.createElement(
type,
[props],
[...children]
)Эта функция принимает три аргумента:
- Аргумент “type” может быть либо строкой имени тега (например, ‘div’ или ‘span’), либо типом компонента React (класс или функция);
- Вторым аргументом отправляется свойство. “props” содержит список атрибутов, например: class, id и т.д.;
- “children” содержит дочерние выражения нового элемента.
Контроль над этими аргументами даёт злоумышленнику возможность реализовать различные вектора атак.
Внедрение дочерних узлов
В марте 2015 года Даниель Лешеминан опубликовал статью «XSS через поддельный элемент React», в которой сообщалось о хранимой XSS в HackerOne. Проблема была вызвана тем, что веб-приложение HackerOne передавало объект, доступный для изменения пользователем, в качестве дочернего аргумента в функцию React.createElement(). Предположительно, уязвимый код выглядит следующим образом:
- JSX:
/* 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>;
}- JavaScript:
React.createElement("span", null, attacker_supplied_value);Когда «attacker_supplied_value» — строка, как ожидалось, на странице появлялся обычный span-элемент. Однако, функция «createElement()» в текущей версии ReactJS принимает простые объекты, переданные как дочерние. Даниель нашел уязвимость, передавая JSON объект. Он передал в нем поле «dangerouslySetInnerHTML», которое позволило вставить необработанный HTML в вывод React. Окончательный proof-of-concept выглядел так:
{
_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>"
}
}
}В ноябре 2015 года данную проблему решили следующим образом: элементы React были теперь отмечены атрибутом «$$typeof: Symbol.for(‘react.element’)». Поскольку невозможно ссылаться на глобальный JavaScript символ из внедренного объекта, метод инъекции дочерних элементов не работает.
Внедрение Props
Рассмотрим следующий код:
// 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);Здесь мы можем внедрить произвольное свойство (props) в новый элемент. Мы можем использовать следующий код для установки «dangerouslySetInnerHTML»:
{"dangerouslySetInnerHTML" : { "__html": "<img src=x/
onerror='alert(localStorage.access_token)'>"}}“Классическая” XSS
Некоторые традиционные XSS вектора также могут быть исполнены в приложениях с движком React. В данном разделе описаны анти-шаблоны.
Явная настройка «dangerouslySetInnerHTML»
Разработчики могут нарочно установить свойство «dangerouslySetInnerHTML»:
<div dangerouslySetInnerHTML={user_supplied} />Контроль над значением этого свойства дает злоумышленнику внедрить любой JavaScript код.
Внедряемые атрибуты
Контроль над атрибутом «href» динамически сгенерированного тега «a» дает возможность внедрить «javascript:». Некоторые другие атрибуты, такие как «formaction» в HTML5, также работают в современном браузере.
<a href={userinput}>Link</a>
<button form="name" formaction={userinput}>Следующий вектор также будет работать в современных браузерах:
<link rel="import" href={user_supplied}>Рендеринг HTML на стороне сервера
Для улучшения начального времени загрузки страницы в последнее время наблюдается тенденция создавать ReactJS страницы с предварительным рендерингом на сервере. В ноябре 2016 года Эмилия Смит отметила, что официальный пример Redux кода для серверного рендеринга приводит к XSS, так как состояние клиента было соединено со страницей, так как клиентская часть подготавливается к рендерингу без санитизации данных (с тех пор пример был исправлен).

Вывод: если сделать пререндеринг HTML страницы на стороне сервера, то можно заметить те же типы XSS, как и в “обычных” веб-приложениях.
Вредоносная полезная нагрузка:
preloadedState = {
attacker_supplied :
"xss</script><script>alert(1)</script>"
}CSS-in-JS
CSS-in-JS — новая технология, которая полностью устраняет необходимость в именах классов CSS.Это позволяет добавлять стили непосредственно к компонентам, используя всю мощь CSS. Но к сожалению, также открывается возможность атакам типа «инъекции».
Атаки с использованием CSS являются серьезной проблемой безопасности.
Если разработчик не будет следовать одному простому правилу: «никогда не интерполируйте ввод пользователя в свои таблицы стилей», то он может непреднамеренно разрешить злоумышленникам анализировать данные пользователей, украсть их учетные данные, выполнить произвольный код на JavaScript и т.д.
Password stealing color
Предположим, что нужно разрешить пользователям выбирать цвет своей страницы профиля. Простым CSS это было бы довольно трудно, но CSS-in-JS упрощает работу. Бэкэнд-разработчик обработал API, и теперь есть цветовая поддержка в стилизованных компонентах.
Поскольку приложение является одностраничным, форма логина открывается как оверлей над профилем. И так как бэкэнд-разработчик сохранил цвет в текстовом поле без проверки, злоумышленник теперь может установить цвет, который может украсть некоторые пароли пользователей:

Это работает при использовании селекторов атрибутов в поле пароля для изменения фонового изображения в зависимости от текущего ввода. После ввода пароля в консоли браузера выводится следующее:

Хотя данная атака не может украсть все пароли, но несколько украденных паролей более чем достаточно.
Кража данных
Данная атака на основе CSS заключается в злоупотреблении unicode-диапазоном @font-face. С помощью этой техники, злоумышленник может читать текст страницы:

Когда вы получаете доступ к этой странице, браузер извлекает «?A» и «?B», так как текстовая часть чувствительной информации содержит символы «A» и «B». Но не извлекается «?C», так как там не содержится символ «C». Это означает, что мы смогли прочитать «A» и «B».
Выводы
Несмотря на то, что ReactJS вполне безопасен по дизайну, плохая практика программирования может привести к уязвимостям безопасности.
- Пентестерам: пробуйте внедрить JavaScript, JSON и CSS везде где можно и смотрите результат;
- Разработчикам: никогда не используйте «eval()» или «dangerouslySetInnerHTML». Избегайте разбора пользовательского JSON. Никогда не интерполируйте ввод пользователя в свои таблицы стилей.
P.S. Данная статья была написана на основе статей Бернарда Мюллера и Джеймса Нельсона.