XSS и контекст вывода
Сервер умеет принять название и текст заметки, но принятие строки не определяет её дальнейший смысл. Если вставить значение в HTML как разметку, браузер может создать новые элементы. Cross-site scripting, или XSS, возникает, когда недоверенные данные получают возможность исполняться в контексте страницы.
В нашей библиотеке заметки являются обычным текстом. Поэтому результат урока простой и проверяемый: название с угловыми скобками отображается буквально, а не меняет устройство документа. Для HTML-редактора потребовалась бы другая политика; сейчас такой возможности в проекте нет.
Контекст вывода
Одна строка может оказаться между тегами, внутри атрибута, в URL или в JavaScript. Эти места имеют разные правила. Преобразование для обычного HTML-текста не делает строку подходящим адресом ссылки и не разрешает использовать её как программу.
Начнём с названия заметки. Учебное значение <b>План</b> должно показывать символы вместе с угловыми скобками. Если слово становится жирным и скобки исчезают, приложение уже интерпретирует пользовательский текст как HTML. Для проверки механизма не требуется более сложный исполняемый пример.
В Flask используем шаблон с расширением .html и стандартным автоматическим экранированием Jinja. Данные передаются отдельным аргументом, а не подставляются в исходник шаблона. Это важное различие: строка должна оставаться значением переменной.
Шаблон заметки
Создайте templates/note.html. Ниже содержательный фрагмент шаблона; общая оболочка позже будет вынесена в base.html.
<article>
<h1>{{ note["title"] }}</h1>
<p class="note-text">{{ note["text"] }}</p>
</article>
Обработчик выбирает запись и передаёт её render_template. В начальном варианте это ещё публичный вымышленный словарь:
from flask import render_template
@app.get("/notes/<int:note_id>")
def note(note_id):
item = NOTES.get(note_id)
if item is None:
abort(404)
return render_template("note.html", note=item)
Этот обработчик заменяет одноимённый JSON-маршрут base.py, а не регистрируется рядом с ним. Проверка владельца появится отдельно. Изменение способа вывода не добавляет авторизацию, даже если страница стала выглядеть аккуратнее.
Автоэкранирование преобразует специальные символы для HTML-контекста. Исходный текст в словаре или базе менять не нужно. Если предварительно сохранить HTML-сущности, следующий вывод может показать их повторно экранированными. Данные и представление полезно хранить раздельно.
Вывод через JavaScript
Та же граница существует в браузере. Если заметка пришла через API, для обычного элемента используйте textContent. Ниже самостоятельный фрагмент для существующего заголовка #note-title:
const title = document.querySelector("#note-title");
title.textContent = note.title;
Переменная note здесь является объектом ответа API. Код не должен использовать innerHTML ради отображения простой строки. В этом случае содержимое становится текстовым узлом и не разбирается как набор тегов.
Но textContent не является универсальным разрешением для любого элемента. Если поместить строку в script, задача уже относится к программному контексту. Аналогично атрибут href требует правил адреса, а не только выбора DOM-операции. Рекомендации по этим различиям приведены в OWASP XSS Prevention.
Наша финальная версия не вставляет пользовательские значения в исходник JavaScript. Скрипты остаются отдельными доверенными файлами. Это упрощает и контекст вывода, и будущую политику CSP: странице не нужны динамически созданные программы.
Данные из базы остаются данными
Название может быть сохранено сегодня и показано через неделю другому браузеру. Такое хранение не повышает доверие к HTML-содержимому. Защита должна действовать при каждом выводе, включая список заметок, страницу одной записи и форму изменения.
В форме значение title помещается в обычный заключённый в кавычки атрибут value через шаблон. Основной текст выводится внутри textarea. Не добавляйте фильтр safe к этим значениям ради сохранения видимых скобок: корректное экранирование уже позволяет показать их пользователю.
Если пользовательский текст содержит ссылку, приложение пока показывает её как текст. Автоматическое превращение найденных адресов в ссылки вводит новую возможность. Понадобятся разрешённые схемы, проверка URL и безопасное построение элемента. Не включайте такой механизм незаметно в функцию отображения заметки.
Когда нужен HTML
Иногда продукт действительно предоставляет форматированный редактор. Тогда обычное экранирование покажет разметку буквально и не выполнит требование продукта. Для такого режима нужна явно заданная допустимая структура и поддерживаемая библиотека очистки HTML.
Это отдельная функция, а не причина отключить экранирование во всём приложении. Даже очищенный HTML имеет ограниченный договор: последующее изменение строки или вставка в другой контекст может его нарушить. Для нашего небольшого стенда обычного текста достаточно, поэтому дополнительный обработчик не вводим.
Нельзя оценивать защиту только по одному успешно показанному названию. Просмотрите все места вывода: список, отдельную заметку, поле формы, сообщение после сохранения. Новый маршрут API тоже может добавить свой клиентский способ отображения. Общий шаблон помогает сохранить правило, но не проверяет каждый будущий обработчик автоматически.
Наблюдение результата
Поменяйте учебное название на строку с тегом b и откройте заметку. Ожидается буквальный текст. В дереве DOM не должен появиться пользовательский элемент b. Затем откройте форму изменения и убедитесь, что значение сохраняется полностью и не разрывает атрибут.
Не называйте отсутствие всплывающего окна доказательством всей защиты: часть ошибок проявляется иначе. В нашем сценарии проверяется конкретное требование — обычный текст остаётся текстом в заранее определённых местах. Для других контекстов понадобятся другие наблюдения.
Поле изменения и исходные байты
Сравните отображение названия с двойной кавычкой в карточке и в input формы. В первом случае значение находится между тегами, во втором — внутри заключённого в кавычки атрибута. Автоэкранирование шаблона сохраняет структуру в обоих обычных контекстах, а исходное название остаётся одной строкой в данных.
Аналогично текст с буквальной последовательностью закрытия textarea не должен создавать новый элемент формы. Шаблон отображает её как содержание поля. Проверка здесь относится к устройству DOM, поэтому важнее посмотреть реальные элементы, чем только внешнее впечатление страницы.
Не делайте предварительное «исправление HTML» в read_note_form. Иначе редактор может получить другое значение при каждом повторном сохранении. Защита вывода применяется каждый раз и не должна требовать хранить представление конкретного шаблона в базе.
Наконец, выбранный PNG-режим вложений не предоставляет загрузку SVG или HTML. Если такая возможность появится, у неё будет собственный активный контекст браузера. Текстовая защита title не распространяется автоматически на новый тип документа.
Теперь принятая строка имеет понятный путь к странице. Следующий урок добавит политику ресурсов и защитные заголовки браузера, объяснив, какие последствия они ограничивают и почему не заменяют правильный вывод текста.