habrahabr

Почему у React элементов есть свойство $$typeof?

  • суббота, 8 декабря 2018 г. в 00:18:35
https://habr.com/post/432350/
  • JavaScript
  • ReactJS
  • Информационная безопасность
  • Разработка веб-сайтов


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