О механизме 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».