О механизме React по предотвращению возможности инъек��ии JSON для XSS, и об избегании типовых уязвимостей.
Вы можете подумать, что вы пишете JSX:
<marquee bgcolor="#ffa7c4">hi</marquee>
Но на самом деле вы вызываете функцию:
React.createElement( /* type */ 'marquee', /* props */ { bgcolor: '#ffa7c4' }, /* children */ 'hi' )
И эта функция возвращает вам обычный объект, который называется React элементом. Соответственно, после обхода всех компонентов, получается дерево из подобных объектов:
{ type: 'marquee', props: { bgcolor: '#ffa7c4', children: 'hi', }, key: null, ref: null, $$typeof: Symbol.for('react.element'), }
Если вы использовали ранее React, вы можете быть знакомы с полями type, props, key и ref. Но что за свойство $$typeof? И почему у него в качестве значения символ Symbol()?
До того, как UI библиотеки стали популярными, для отображения клиентского ввода в коде приложения генерировали строку содержащую HTML разметку и вставляли ее напрямую в DOM, через innerHTML:
const messageEl = document.getElementById('message'); messageEl.innerHTML = '<p>' + message.text + '</p>';
Такой механизм отлично работает, за исключением случаев, когда message.text имеет значение <img src onerror="stealYourPassword()">. Соответственно приходим к выводу, что не нужно интерпретировать весь клиентский ввод, как HTML разметку.
Для защиты от подобных атак можно использовать безопасные API, такие как document.createTextNode() или textContent, которые не интерпретируют текст. И в качестве дополнительной меры экранировать строки, заменяя потенциально опасные символы, такие как <, > на безопасные.
Тем не менее, вероятность ошибки высока, так как трудно отслеживать все места, где вы используете записанную пользователем информацию в своей странице. Вот почему современные библиотеки, такие как React безопасно работают с любым текстом по умолчанию:
<p> {message.text} </p>
Если message.text — это вредоносная строка с тегом <img>, она не превратится в настоящий тег <img>. React экранирует текстовое содержимое, а затем добавит его в DOM. Поэтому вместо того, чтобы видеть тег <img>, вы просто увидите его разметку как строку.
Чтобы отобразить произвольный HTML внутри элемента React, вы должны воспользоваться следующей конструкцией: dangerouslySetInnerHTML={{ __html: message.text }}. Конструкция намеренно неудобная. Благодаря своей несуразности она становиться более заметной, и привлекает внимание при просмотре кода.
Означает ли это, что React полностью безопасен? Нет. Известно множество способов атак, в основе которых используются HTML и DOM. Особого внимания заслуживают атрибуты тегов. Например, если вы напишите <a href={user.website}>, то можно в качестве текстовой ссылки подставить вредоносный код: 'javascript: stealYourPassword()'.
В большинстве случаев, наличие уязвимостей на клиентской части, являются следствием проблем с серверной стороны, и прежде всего должны исправляться на ней.
Тем не менее, безопасное отображение пользовательского текстового контента, это разумная первая линия защиты, которая отражает множество потенциальных атак.
Осно��ываясь на предыдущих рассуждениях, можно сделать вывод, что следующий код должен быть полностью безопасен:
// Автоматическое экранирование <p> {message.text} </p>
Но, это тоже не так. И тут мы подбираемся ближе к объяснению присутствия $$typeof в элементе React.
Как мы выяснили ранее, React элементы — простые объекты:
{ type: 'marquee', props: { bgcolor: '#ffa7c4', children: 'hi', }, key: null, ref: null, $$typeof: Symbol.for('react.element'), }
Обычно React элемент создается с помощью вызова функции React.createElement(), но можно создать его сразу литералом, как я только что сделал выше.
Предположим, что мы храним на сервере строку, которую нам ранее отправил пользователь, и каждый раз отображаем ее на клиентской части. Но кто-то вместо строки отправил нам JSON:
let expectedTextButGotJSON = { type: 'div', props: { dangerouslySetInnerHTML: { __html: '/* здесь пишем вредоносный код */' }, }, // ... }; let message = { text: expectedTextButGotJSON }; // Опасный момент для React 0.13 <p> {message.text} </p>
То есть внезапно вместо ожидаемой строки в качестве значения переменной expectedTextButGotJSON оказался JSON. Который будет обработан React'ом как литерал, и тем самым исполнит вредоносный код.
React 0.13 уязвим для подобной XSS атаки, но начиная с версии 0.14 каждый элемент помечается символом:
{ type: 'marquee', props: { bgcolor: '#ffa7c4', children: 'hi', }, key: null, ref: null, $$typeof: Symbol.for('react.element'), }
Подобная защита работает, потому что символы не являются валидным значением JSON. Поэтому, даже если сервер имеет потенциальную уязвимость и возвращает JSON вместо текста, JSON не может содержать Symbol.for('response.element'). React проверяет элемент на наличие element.$$typeof и откажется обрабатывать элемент, если он отсутствует или недействителен.
Главное преимущество Symbol.for() заключается в том, что символы являются глобальными между контекстами, потому что используют глобальный реестр. Тем самым обеспечивают одинаковое возвращаемое значение даже в iframe. И даже если на странице имеется несколько копий React, они все равно смогут «солгасоваться» через единое значение $$typeof.
А что с браузерами, которые не поддерживают символы?
Увы, они не смогут реализовать рассмотренную выше дополнительную защиту, но React элементы все равно будут содержать свойство $$typeof для согласованности, но оно будет просто числом — 0xeac7.
Почему именно 0xeac7? Потому что выглядит как «React».
