Перенаправления и адреса
После входа посетителя удобно вернуть к нужному месту. Часто для этого используется параметр next. Если приложение без проверки отправляет его в Location, собственный домен начинает перенаправлять людей на любой адрес, выбранный отправителем ссылки.
В security-lab потребность гораздо меньше: после входа нужно открыть список заметок или проверку здоровья. Результат урока — выбор из известных маршрутов вместо разбора произвольного внешнего URL. Такое ограничение следует из функции продукта, а не из попытки составить бесконечный список плохих адресов.
Адрес как команда перехода
Location является частью HTTP-ответа, которую браузер использует для следующего обращения. Строка параметра перестаёт быть текстом подписи и получает навигационное значение. Поэтому экранирование HTML из урока XSS не делает её разрешённой целью.
Например, значение https://example.org указывает внешний origin. Строка, начинающаяся с двух слешей, может быть сетевым адресом без явной схемы. Проверка только startswith("/") не описывает все формы URL. Но нашей библиотеке вообще не требуется принимать их.
Вместо next с полным адресом определим короткие ключи: notes и health. Словарь сопоставляет им внутренние endpoint Flask. Пользователь выбирает возможность, которую продукт заранее предоставляет.
Разрешённые маршруты
Ниже полный helper заключительного приложения. Он возвращает относительный путь через url_for. Неизвестное значение приводит к обычному списку заметок.
from flask import url_for
RETURN_ENDPOINTS = {"notes": "notes", "health": "health"}
def return_path(key):
endpoint = RETURN_ENDPOINTS.get(key, "notes")
return url_for(endpoint)
Ветка успешного входа получает короткий ключ из формы и вызывает redirect с этим результатом:
target = request.form.get("next", "notes")
return redirect(return_path(target))
Имя endpoint не берётся напрямую из запроса. Это важно: приложение может иметь внутренние маршруты, на которые нет нужды возвращать посетителя. Словарь выражает именно разрешённый набор функции.
Мы также не используем _external=True для этой цели. Относительный путь не требует построить внешний адрес по заголовку Host. Проверка доверенных хостов и прокси всё равно понадобится для других функций, но текущий переход имеет меньший договор.
Форма и повторный вход
Форма входа сохраняет next как ключ, а не URL. Скрытое поле остаётся пользовательским вводом, поэтому сервер повторно применяет словарь. Наличие нашего HTML не превращает значение в доверенное.
<input type="hidden" name="next" value="{{ next_key }}">
Next_key подготовлен для отображения разрешённого ключа. Даже если посетитель изменит поле в инструментах браузера, helper не вернёт внешний адрес. Экранирование шаблона относится к правильной структуре input, словарь — к смыслу навигации.
При неверном пароле полезно оставить выбранный разрешённый ключ, чтобы повтор не потерял намерение. Но нельзя отражать исходный опасный адрес как кликабельную ссылку в ошибке. Ошибка входа должна оставаться обычным текстом с понятной дальнейшей формой.
Наблюдение поведения
Для notes ожидается переход к /notes. Для health — к /health. Для полного внешнего URL, двух слешей или неизвестного ключа — к /notes. Это простая таблица, которую легко сопоставить с функцией.
| Значение | Ожидаемая цель |
|---|---|
| notes | /notes |
| health | /health |
| https://example.org | /notes |
| //example.org | /notes |
| отсутствует | /notes |
Ответ Location нужно посмотреть отдельно от последнего экрана браузера. Автоматический переход может скрыть промежуточное решение. Если конечная страница открылась, это ещё не доказывает, что сервер правильно обработал исходный параметр.
Рекомендации о таком классе ошибок собраны в OWASP Unvalidated Redirects. Наш вариант избегает большого парсера именно потому, что продукту достаточно двух известных действий.
Ссылки пользовательского содержания
Похожий вопрос возникает, если заметка содержит адрес. Пока наша библиотека показывает его как текст. Для будущей кликабельной ссылки потребуется отдельное разрешение схем и назначений, а также безопасное создание элемента.
Не считайте helper return_path универсальным фильтром ссылок. Он работает с ключом известной операции, а не с полным URL. Если передать ему любую строку статьи, результатом будет список заметок, что не соответствует задаче редактора ссылок.
Также внутренний путь не означает право доступа. После возвращения на /notes сервер заново проверяет сессию и выбирает собственные записи. Перенаправление только приводит браузер к маршруту; оно не должно обходить middleware или передавать чужую личность.
Изменение требований
Допустим, позже понадобится возвращаться к конкретной заметке. Можно принять целочисленный ID, построить путь через url_for и при открытии применить note_for_user. При этом ключ операции и параметр объекта имеют свои типы и пределы.
Если действительно нужен внешний переход, модель изменится. Потребуется утверждённый набор получателей, корректный разбор адреса и политика подтверждения. Но не стоит вводить такие возможности ради гипотетического будущего, оставляя текущий login с произвольным Location.
Сохраняйте договор рядом с формой и helper. Когда другой разработчик добавит новый endpoint, он сможет решить, относится ли он к возвращению после входа. Простое расширение словаря тогда будет видимым изменением функции.
Неявные преобразования адреса
Процентное кодирование, фрагмент и параметры являются частями устройства URL. Если продукт принимает полный адрес, их нужно рассматривать после корректного разбора, а не по внешнему виду строки. В нашем договоре они вообще не соответствуют ключам notes и health, поэтому попадут в обычную цель.
Важно проверить Location без автоматического следования. Клиент, который сразу показывает последнюю страницу, способен скрыть сам факт промежуточного внешнего перехода. Наблюдение заголовка относится именно к решению обработчика.
Скрытое поле next тоже имеет структуру. В финальном POST-входе повторные значения этого поля отклоняются. Даже хотя обе возможные цели внутренние, понятный договор помогает избежать различия между кодом формы и обработчиком.
Если пользователь пришёл по ссылке с неправильным ключом, сообщение формы не должно создавать кнопку с исходной строкой «для продолжения». Иначе безопасный серверный redirect будет соседствовать с отдельной небезопасной ссылкой шаблона.
Конфигурация внешнего хоста становится особенно важной для ссылок, которые отправляются вне текущего браузера. Мы их пока не создаём. Перенос helper к email-ссылке без пересмотра требований добавил бы новый контекст, не охваченный этим уроком.
Теперь удобная навигация после входа не создаёт произвольной команды перехода. Следующий урок добавит изображение владельца и покажет, почему имя файла, объявленный MIME и реальное содержимое требуют разных проверок.