Три типовых ошибки в сфере безопасности, о которых должен знать каждый React-разработчик

Автор статьи, перевод которой мы сегодня публикуем, говорит, что React — это её любимая библиотека для создания интерактивных интерфейсов. React одновременно и лёгок в использовании, и достаточно хорошо защищён. Однако это не значит, что React-приложения совершенно неуязвимы. Очень легко впасть в неоправданное спокойствие, решив, что о XSS-атаках можно не волноваться из-за того, что в проекте используется React.

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

Сегодня мы поговорим о типичных уязвимостях React, о том, как находить их во время код-ревью, и о том, как от них защищаться.

Первый (очень короткий) пример, касающийся межсайтового скриптинга

Межсайтовый скриптинг (Cross site scripting, XSS) — это клиентская уязвимость, которая способна привести к серьёзным проблемам. XSS-атаки происходят тогда, когда злоумышленник способен обмануть веб-сайт и заставить его выполнять произвольный JavaScript-код в браузерах его пользователей.

Отражённая (reflected) XSS-атака выполняется посредством ссылки, содержащей текстовую информацию, которая обрабатывается браузером в виде кода. Например — это поле формы, в которое, на стороне клиента, введён особый текст запроса.

Хранимая (stored) XSS-атака — это ситуация, в которой атакующий имеет доступ к серверу, и когда код, выполняемый на сервере, формирует то, что попадает на веб-страницу клиента. Типичными векторами подобных атак является загрузка на серверы комментариев и изображений.

Червь Samy эксплуатировал XSS-уязвимость MySpace. Это был один из самых быстрораспространяющихся вирусов всех времён.

Уязвимые веб-сайты могут подвергнуть своих пользователей риску кражи паролей или персональных данных. И это — обычный способ эксплуатации других уязвимостей. Вредоносные скрипты чаще всего используются для рассылки спама и для перенаправления пользователей на мошеннические сайты. Подобное может повредить репутации и SEO-показателям успешно атакованного сайта.

Уязвимость №1: контроль над исходным состоянием страницы, которое используется в ходе серверного рендеринга

Иногда, когда мы формируем исходное состояние страницы, мы, что опасно, создаём документ на основе JSON-строки. Эта уязвимость выглядит в коде примерно так:

<script>window.__STATE__ = ${JSON.stringify({ data })}</script>

Это опасно из-за того, что метод JSON.stringify(), ни о чём не «думая», преобразует любые предоставленные ему данные в строковую форму (до тех пор, пока это — валидные JSON-данные), представляющую собой то, что будет выведено на странице. Если у { data } есть поля, которые может редактировать недоверенный пользователь, вроде имени пользователя или сведений о пользователе, в подобные поля можно внедрить нечто, подобное следующему:

{ username: "pwned", bio: "</script><script>alert('XSS Vulnerability!')</script>" }

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

Не верите? Тогда убедитесь в этом сами. Поищите в Google по тексту «react server side rendering example app» и попробуйте выполнить эту атаку на любом из результатов поиска с первой страницы.

▍Выявление уязвимости во время код-ревью

Ищите вызовы метода JSON.stringify(), принимающие переменные, которые могут содержать недоверенные данные в теге script. Вот пример, который раньше был в документации Redux:

function renderFullPage(html, preloadedState) {     return `         <!doctype html>         <html>             <head>                 <title>Redux Universal Example</title>             </head>             <body>                 <div id="root">${html}</div>                 <script>                     window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)}                 </script>                 <script src="/static/bundle.js"></script>             </body>         </html>         ` }

А вот — кусок кода из приложения-примера, который нашёлся на GitHub:

function htmlTemplate( reactDom, reduxState, helmetData ) {     return `     <!DOCTYPE html>     <html>     <head>         <meta charset="utf-8">         ${ helmetData.title.toString( ) }         ${ helmetData.meta.toString( ) }         <title>React SSR</title>         <link rel="stylesheet" type="text/css" href="./styles.css" />     </head>          <body>         <div id="app">${ reactDom }</div>         <script>             window.REDUX_DATA = ${ JSON.stringify( reduxState ) }         </script>         <script src="./app.bundle.js"></script>     </body>     </html>     `;      }

Иногда обнаружить эту уязвимость немного сложнее. Следующий код тоже окажется небезопасным — в том случае, если не выполнено правильное экранирование context.data:

const RenderedApp = htmlData.replace('{{SSR}}', markup)     .replace('<meta-head/>', headMarkup)     .replace('{{data}}', new ArrayBuffer(JSON.stringify(context.data)).toString('base64'))

При выполнении серверного рендеринга обращайте внимание на то, что именно рендерится. Если то, что вводит пользователь, не экранировано соответствующим образом и выводится в DOM — это может быть опасным.

▍Защита

один из вариантов защиты от этой уязвимости заключается в использовании npm-модуля serialize-javascript, который предназначен для экранирования выводимого JSON. Если вы выполняете серверный рендеринг не в среде Node.js — то вам понадобится самостоятельно подобрать соответствующий пакет для используемого вами языка.

Вот команда для установки модуля:

$ npm install --save serialize-javascript

После этого его надо импортировать в файл и следующим образом переписать ранее уязвимый код, имеющий дело с window:

<script>window.__STATE__ = ${ serialize( data, { isJSON: true } ) }</script>

Вот отличная статья, посвящённая этому вопросу.

Уязвимость №2: коварные ссылки

Тег <a> может иметь атрибут href, который содержит ссылку на другую страницу сайта, на другой сайт, на некое место текущей страницы. Ссылки могут содержать и скрипты, которые выглядят примерно так: javascript: stuff(). Если вы не знали об этой возможности HTML — опробуйте её прямо сейчас, скопировав в строку браузера следующий код:

data:text/html, <a href="javascript: alert('hello from javascript!')" >click me</a>

Для веб-разработчиков это означает, что если содержимое ссылок устанавливается на основе данных, введённых пользователем — атакующий может добавить в эти данные вредоносный код, начинающийся с javascript:. Затем, если пользователь щёлкнет по нехорошей ссылке, в браузере будет запущен скрипт злоумышленника.

Эта уязвимость, определённо, характерна не только для React, но это одна из тех проблем, с которой часто сталкиваются React-разработчики, когда ожидают, что соответствующее значение должно быть автоматически правильно экранировано. Надо отметить, что в будущей версии React эта проблема будет стоять уже менее остро.

▍Выявление уязвимости во время код-ревью

Могут ли пользователи проекта добавлять на страницы ссылки, по которым могут щёлкать другие пользователи? Если это так — попробуйте добавить на страницу «ссылку» наподобие следующей:

javascript: alert("You are vulnerable to XSS!")

Если при щелчке по ссылке будет показано соответствующее окно сообщения — это значит, что проект уязвим к XSS-атакам. Попробуйте это везде, где можно добавлять ссылки. Вполне вероятно, что уязвимости окажутся подвержены не все подобные места.

▍Защита

Защита от этой уязвимости подходит не только для React-проектов. То, что именно нужно сделать, зависит от приложения. Кроме того, возможно, исправления нужно будет вносить на сервере.

Можно подумать, что для решения проблемы достаточно удалить из данных префикс javascript:. Это — пример использования стратегии «чёрного списка», которую нельзя признать удачной в деле очистки данных. У хакеров есть хитроумные способы обхода подобных фильтров, поэтому вместо подобного хода (или в дополнение к нему) сделайте так, чтобы ссылки использовали бы протокол, контролируемый по принципу «белого списка» (например — http:) и экранируйте HTML-сущности. Вот подробная статья на эту тему, касающаяся экранирования свойств, которые может контролировать злоумышленник.

Ещё одной стратегией, которая способна добавить в проект дополнительный уровень защиты, является использования механизма открытия пользовательских ссылок в новых вкладках браузера. Я, однако, не рекомендовала бы использовать эту стратегию как единственную «линию обороны» проекта. Открытие javascript:-ссылки в новой вкладке — это пример нестандартного поведения элементов страницы. Большинство браузеров без вреда для пользователя выполнят скрипт в пустой вкладке, но это не гарантировано, и, вероятно, это можно обойти, что зависит от браузера.

Рассмотрите возможность использования специального компонента UserLink, что приведёт к тому, что уязвимый тег <a> с меньшей долей вероятности попадёт на страницы вашего проекта в будущем. Кроме того, стоит добавить в проект несколько тестов и правил линтинга, нацеленных на выявление потенциально опасного кода и на то, чтобы не допустить его попадания в продакшн.

Ссылки — это не единственные сущности, которые могут быть использованы подобным образом. Но они — наиболее вероятная цель атаки в React-приложениях. Любой элемент может быть уязвимым к данной атаке в том случае, если злоумышленник может управлять его значением URI. Ещё одной возможностью проведения этой атаки, например, является конструкция вида . Полный список атрибутов, которые могут содержать URI, можно найти в этом списке по ключевому слову %URI, воспользовавшись браузерным поиском (Ctrl+F).

Уязвимость №3: непонимание смысла конструкции dangerouslySetInnerHtml

Я чрезвычайно благодарна React за то, что предупреждение о безопасности находится прямо в имени метода. Это — имя dangerouslySetInnerHTML. Мы, несмотря на это предупреждение, всё ещё часто сталкиваемся с тем, что разработчики рискуют, выполняя небезопасные операции. То же самое можно сказать и об eval().

Рассмотрим следующий пример, который я обнаружила на сайте с первой страницы поисковой выдачи Google:

<script dangerouslySetInnerHTML={{ __html: `window.__PRELOADED_STATE__ = ${JSON.stringify(initialState)};`}}></script>

Это — пример уязвимости №1, но с одной особенностью, которая должна сразу привлечь внимание к тому, что тут что-то не так. Там, где я это нашла, сделана попытка объясниться: «Мы используем dangerouslySetInnerHTML в качестве метода очистки данных и предотвращения XSS-атак». Ну уж нет! Это неправильно. Не делайте так. Для того чтобы узнать подробности о dangerouslySetInnerHTML — почитайте документацию React.

Ещё один пример того, что подобное, на самом деле, происходит постоянно — то, как члены одной команды обнаружили, что у них имеется уязвимость, когда добавляли на страницу Markdown-разметку, используя dangerouslySetInnerHTML. Для того чтобы обезопасить себя от этого в будущем — они стали пользоваться специальным правилом линтинга.

▍Выявление уязвимости во время код-ревью

Прежде чем отправлять pull-запросы или выполнять операции слияния — полезно выполнить поиск в коде по строкам dangerouslySetInnerHTML и eval (я, кроме того, ищу так команды console.log) или воспользоваться соответствующим правилом линтера.

▍Защита

Проверьте, чтобы во всех случаях использования метода dangerouslySetInnerHTML выполнялась бы загрузка на страницу исключительно данных, которым вы можете доверять. Как узнать о том, что данным можно доверять? Если нечто пришло не от вас — это нечто может нести в себе угрозу. Сюда входят и данные, загруженные из внешних API, и то, что оформляется средствами Markdown.

Замечание о спуфинге компонентов

В 2015 году некто выяснил, что можно спуфить компоненты, передавая JSON тем компонентам, которые ожидают текст. Я смогла найти лишь один случай сообщения о спуфинге компонентов и длинное обсуждение, вызванное этим сообщением. В обсуждении речь шла о том, какова мера ответственности React в деле предотвращения XSS-атак. В итоге разработчики React выпустили исправление, которое, как кажется, способствовало устранению этой уязвимости.

Я решила не включать рассказ об этой уязвимости в статью, но эта тема может представлять интерес для дальнейших исследований.

Замечание о SSR

Уязвимость, касающаяся серверного рендеринга, столь широко распространена из-за того, что она присутствовала в документации Redux и в результате разошлась по множеству других материалов. Эту проблему исправили в 2016 году. Но и сегодня, через три года, в руководствах для новичков, разбросанных по всему интернету, всё ещё учат тому, чему учить не стоило бы.

Кстати, вот вам домашнее задание: найдите пример этой проблемы на GitHub и отправьте pull-запрос на её исправление. Вот пример.

Вместе мы сможем раз и навсегда избавиться от этой уязвимости!

Уважаемые читатели! Сталкивались ли вы с атаками на ваши React-проекты?


ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/467255/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *