CSP и защитные заголовки
Вывод заметки уже сохраняет пользовательское содержание как текст. Теперь добавим правила, которые браузер получает вместе с HTTP-ответом. Они ограничивают источники ресурсов, размещение страницы во фрейме и передачу лишних сведений при переходе. Такой слой полезен, если его договор соответствует устройству приложения.
Результат урока — набор заголовков для security-lab и разбор их действия. Политика не исправляет неправильный SQL или отсутствие проверки владельца. Она задаёт возможности браузера после получения страницы и дополняет уже введённые границы.
Ресурсы нашей страницы
Учебному приложению нужны собственные стили, обычные формы и изображения из своего маршрута. Оно не загружает внешние скрипты, не встраивает произвольные фреймы и не использует динамический JavaScript в HTML. Значит, список разрешённых источников можно сделать небольшим.
Content Security Policy, или CSP, сообщает браузеру, откуда разрешено получать разные виды ресурсов. Политика относится к документу, а не к содержимому базы. Для нашего случая используем собственный origin для стилей и изображений, полностью запретим объекты и ограничим отправку форм тем же origin.
Внешний файл style.css подключается обычным link. Обработчики onclick в разметке и встроенный script здесь не нужны. Такой выбор делает политику понятной: оформление не требует постоянного добавления исключений для каждой страницы.
Заголовки ответа
Ниже функция after_request заключительного приложения. Она получает сформированный ответ и добавляет заголовки. В Flask регистрируется один такой обработчик, а не копия в каждом маршруте.
@app.after_request
def browser_headers(response):
response.headers["Content-Security-Policy"] = (
"default-src 'none'; "
"style-src 'self'; img-src 'self'; "
"script-src 'self'; connect-src 'self'; "
"base-uri 'none'; form-action 'self'; "
"frame-ancestors 'none'; object-src 'none'"
)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "no-referrer"
response.headers["Cache-Control"] = "private, no-store"
return response
Default-src запрещает ресурсы, для которых не задано подходящее отдельное правило. Некоторые директивы не наследуют этот fallback, поэтому base-uri, form-action и frame-ancestors указаны явно. Их смысл нужно читать по назначению, а не воспринимать строку как универсальный заклинательный набор. Справочник CSP MDN.
Base-uri ограничивает изменение базового адреса документа. Form-action относится к цели отправки формы. Frame-ancestors определяет, кто может встроить нашу страницу; это не то же самое, что список фреймов, которые сама страница могла бы загрузить.
Наблюдение ограничения
Сначала откройте обычную страницу заметки. Собственный CSS должен загрузиться. Затем в отдельном учебном шаблоне временно добавьте встроенный script с безобидным выводом в консоль. При нашей политике ожидается отказ его выполнения и сообщение о нарушении CSP.
Это наблюдение проверяет конкретную директиву. Оно не доказывает отсутствие всех XSS: например, приложение может ошибочно разрешить опасный доверенный скрипт. Правильный вывод из предыдущего урока остаётся необходимым. CSP не заменяет контекстное обращение с данными.
После эксперимента верните шаблон к общему состоянию. В финальном исходнике такого встроенного скрипта нет. Не оставляйте временные ослабления политики, чтобы избавиться от красных строк консоли: сначала определите, какое реальное действие требует ресурса.
Для внедрения политики в большое приложение иногда полезен режим Report-Only, который сообщает о нарушениях без блокировки. На нашем маленьком стенде список ресурсов известен заранее. Публичный сбор отчётов и их очистка являются отдельной системой; мы её не разворачиваем.
Тип содержимого и встраивание
Nosniff просит браузер уважать объявленный тип ресурса. Поэтому сервер обязан возвращать корректный Content-Type. Заголовок не делает произвольный загруженный файл изображением: содержимое и способ выдачи будут проверяться в уроках о вложениях.
Frame-ancestors none и X-Frame-Options DENY ограничивают встраивание страницы. Это помогает против нежелательного интерфейса поверх нашего документа. Но если продукт специально работает внутри доверенного портала, такая политика может нарушить его функцию. Требование должно происходить из модели приложения.
Referrer-Policy no-referrer уменьшает передачу адреса исходной страницы при переходе. Это уместно для личных заметок. Он не удаляет историю самого браузера и не скрывает сетевой запрос от сервера назначения. Не стоит приписывать одному заголовку несвязанные свойства.
Кеширование личных данных
Ответы личного приложения получают private, no-store. Общий кеш не должен хранить страницу Alice как публичный ресурс. Это отличается от статических общедоступных уроков ProfessorWeb, где кеширование помогает быстрой загрузке.
Политика HTTP-кеша не отзывает уже показанный экран и не стирает снимок пользователя. Она задаёт договор хранения ответа для участвующих HTTP-кешей. Права должны проверяться при новом запросе независимо от наличия этого заголовка.
Применять no-store ко всем стилям в большой системе необязательно. Здесь это простой общий вариант небольшого стенда. В дальнейшем можно разделить неизменяемые публичные ресурсы и личные ответы, сохранив ясное правило для последних.
Транспорт и дальнейшие изменения
HSTS требует отдельного решения о HTTPS и домене. На локальном HTTP-стенде мы не используем его как способ получить шифрование. В уроке о транспорте разберём внешний HTTPS, прокси и настройки cookie. Заголовок не создаёт сертификат сам по себе.
При добавлении новой возможности сначала пересмотрите карту ресурсов. Например, внешний аналитический адаптер изменил бы connect-src; встроенное видео потребовало бы другого договора. Не добавляйте общий wildcard заранее ради возможных будущих функций.
Проверьте заголовки успешной страницы и ответа об ошибке. Если политика существует только на одном маршруте, новое окно входа может получить другие возможности. Общий обработчик помогает соблюдать единый договор.
Политика и тип ответа
CSP имеет смысл для того документа, который браузер обрабатывает. Просмотр заголовка у JSON-ответа не показывает, загрузилась ли разрешённая таблица стилей HTML-страницы. Поэтому наблюдение политики выполняется на настоящем шаблоне, где видны соответствующие ресурсы.
Если новый шрифт не загрузился, сначала сопоставьте его источник с font-src или fallback default-src. Не расширяйте все директивы до любого HTTPS только для одной зависимости: это изменит возможности и других ресурсов. Маленькое изменение должно иметь маленькое объяснимое разрешение.
Также проверьте страницу входа и отказа. Форма ошибки могла использовать встроенный стиль или иной внешний ресурс и внезапно перестать работать при общей политике. Такой результат требует исправить шаблон либо обоснованно изменить договор, а не отключить защиту на каждой ошибке.
В нашем учебном интерфейсе CSS является собственным файлом, а пользовательские строки не создают атрибуты style. Именно это исходное устройство позволяет сохранить политику короткой.
Теперь браузер получает явные ограничения вместе с документом. Следующий урок рассмотрит другую границу: превращение принятой строки в значение запроса к SQLite, где защитные заголовки браузера не участвуют.