Автор статьи, перевод которой мы сегодня публикуем, говорит, что 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-проекты?

Комментарии (12)


  1. somurzakov
    13.09.2019 19:52
    +2

    выглядит как будто React-разработчики наступают последовательно на все грабли PHPшников, рендеря небезопасные данные прямо во фронте, уже пора вводить аналог magic_quotes_gpc в реакт?


    1. vvadzim
      16.09.2019 11:45

      Ну… не знаю, может мой опыт слишком специфичен, но я не сталкивался с описанными уязвимостями…

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

      2) ссылки «javascript:» — с пользовательскими url встречался только при загрузке ресурсов на сервер, а там протокол «javascript:» для хацкеров бесполезен.

      3) dangetousSetInnerHtml — всегда жёстко и бесповоротно забанен в проектах с самого начала реакта, ещё даже когда линтеров толком не было :) Меня пытать надо, чтоб это через ревью пропихнуть.

      В целом, информация полезная. Но по-моему, проекты реакта с подобными уязвимостями на массовость не тянут, поэтому ситуация значительно лучше пхп 6-7-летней давности. Про современный пхп я уже ничего не знаю.


      1. Ungla
        16.09.2019 12:49
        +1

        А что не так с dangetousSetInnerHtml, если на сервере данные экранируются и оттуда удаляется всё подозрительное? Как wysiwyg делать?


        1. vvadzim
          16.09.2019 13:57

          Для wysiwig dangerousSetInnerHtml хорош, но в тех проектах что я учавствовал, редакторы жили в отдельном проекте, обычно в пакете из npm. Поэтому именно в прикладных проектах dangetousSetInnerHtml забанен линтером.


  1. lxsmkv
    13.09.2019 23:14

    Все-таки правильно «типичных» (ошибки). Типовой может быть дом, застройка, в смысле шаблонной. А типичный это «свойственный» для кого-то или чего-то.


  1. andres_kovalev
    14.09.2019 01:39
    +1

    Вы приводите подобный пример небезопасного кода.


    &lt;script&gt;window.__STATE__ = ${JSON.stringify({ data })}&lt;/script&gt;

    Допустим даже у нас вот такой стейт:


    { username: "pwned", bio: "&lt;/script&gt;&lt;script&gt;alert('XSS Vulnerability!')&lt;/script&gt;" }

    В чем тут проблема? Даже если какой-то из моих компонентов будет делать что-то подобное:


    return (
      <div>{ state.bio }</div>
    );

    Данное значение все равно будет выведено как текст, а не как html-код. Т.е. "зловредный" скрипт не будет выполнен. Если же речь о чем-то вроде:


    div.innerHTML = state.bio;

    То React уже здесь не при чем.


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


    Не знаю как Вы, но я в своих проектах ещё не встречал использования dangerouslySetInnerHtml, так что не уверен, что это типичная ошибка.


    1. PQR
      14.09.2019 07:46

      Поддерживаю вопрос, совершенно не понятно что автор имел ввиду. Сначала я подумал, что перевод кривой, посмотрел в оригинал — там такое же мутное описание.


      Вообще складывается впечатление, что ru_vsd переводит весь шлак подряд. Иногда встречаются статьи-бриллианты, но какого-то предварительного отсева явно не хватает.


    1. alexesDev
      14.09.2019 10:19

      Речь идет про такой код


      <script>
      const json = JSON.parse('{"text":"</script><script>alert(1)</script>" }')
      ...

      Если вы храните экранированные данные в базе, то у вас нет проблем. Но я не храню и не буду.


      1. andres_kovalev
        14.09.2019 17:13

        Замечательно. Только что с этим кодом не так? Я только что его выполнил в консоли и ничего ужасного не произошло, кроме создания переменной json. Что дальше такого страшного должен сделать "типовой" react-разработчик, чтобы это привело к каким-то нежелательным последствиям? Еще раз отмечу, что действия вроде


        div.innerHTML = json.text;

        никакого отношения к React не имеют.


        1. DarthVictor
          14.09.2019 19:49

          Попробуйте сохранить в server.js содержимое pastebin.com/GVHvwpk1 после чего установить в папку express и выполните

          npm i express
          node server.js

          После этого перейдите на localhost:3000

          div.innerHTML = json.text

          за вас при этом делает шаблонизатор строк


          1. andres_kovalev
            17.09.2019 15:36

            В заголовке статьи написано


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

            Я еще раз повторю на всякий случай — при чем тут React? То, что Вы привели как пример не является проблемой React'а, а самая что ни на есть обычная code-injection (во всем "проверочном" проекте нет ни одной ссылки на React). Единственное, что связывает его с React'ом это то, что такой подход может решить использовать разработчик, который хочет задать исходное состояние для Redux, который вроде как часто используют и с React. До React'а дело и не доходит, а если бы дошло, то там уже никаких проблем бы не было (подобный JSX ни к каким проблемам не приведет <input value={ json.input } />).


        1. alexesDev
          16.09.2019 12:07
          +1

          Сохраните html и откройте как html. Тем что это выведет alert()
          Потому что браузеру плевать, что JS не закончился. Он видит

          и закрывает script. А дальше можно написать любой скрипт. Именно поэтому ошибка такая опасная… ее не видят.