Проверка входных данных
У минимального приложения уже есть понятные маршруты и вымышленные заметки. Теперь введём договор для названия и текста. Посетитель может отправить данные без нашей формы, повторить одно поле несколько раз или передать значительно больше текста, чем ожидает интерфейс. Сервер должен самостоятельно решить, какие значения подходят операции.
Результатом урока станет функция чтения формы с ограничениями и понятным отказом. Она не пытается удалять подозрительные слова. Мы сохраняем обычный текст, а защита SQL и HTML будет выполняться в соответствующих контекстах следующих уроков.
Значение и структура
Проверка начинается со структуры запроса. Для заметки ожидаем один title и один text. Название после удаления внешних пробелов должно содержать от одного до 120 символов; текст — не более 4000. Эти пределы выбраны для небольшого учебного сервиса, а не выведены из универсального стандарта.
Повторное поле имеет неоднозначный смысл. Если один слой выбирает первое значение, другой последнее, результат может различаться. Поэтому в нашем договоре каждое содержательное поле присутствует ровно один раз. Позднее форма получит csrf_token; это служебное поле тоже проверяется своим механизмом.
В Python длина строки отражает число кодовых точек, а не визуальных символов интерфейса. Для нашего ограничения это достаточный учебный договор. Если продукт допускает сложные составные символы и обещает определённое число видимых знаков, потребуется отдельное определение. Проверка байтов тела остаётся другой границей.
Чтение формы
Ниже функция заключительного приложения. Она получает MultiDict, который Flask предоставляет как request.form. Код не обращается к браузеру и не зависит от HTML-атрибута maxlength.
from flask import abort
def read_note_form(form):
allowed = {"title", "text", "csrf_token"}
if set(form) - allowed:
abort(400, description="Неверная форма")
if any(len(form.getlist(key)) != 1 for key in ("title", "text")):
abort(400, description="Неверная форма")
title = form["title"].strip()
text = form["text"]
if not 1 <= len(title) <= 120 or len(text) > 4000:
abort(400, description="Неверная форма")
return title, text
Внешние пробелы названия удаляются по редакционному правилу. Основной текст сохраняется: отступы и переводы строк могут иметь смысл. Это не универсальная нормализация каждого поля. Пароль, например, нельзя незаметно менять таким же способом, поскольку проверка должна относиться к введённому значению.
Разрешённое имя csrf_token не подтверждает подлинность действия. Пока функция только читает поля заметки. Отдельная проверка токена появится в уроке 10 и будет выполняться до изменения. Разделение функций помогает увидеть, какое требование они действительно обеспечивают.
Для самостоятельного наблюдения можно добавить временный маршрут preview в base.py. Он возвращает принятые значения как JSON, не изменяя словарь заметок:
from flask import request
@app.post("/preview")
def preview():
title, text = read_note_form(request.form)
return jsonify(title=title, text=text)
Этот обработчик является учебной демонстрацией ввода. В заключительном приложении вместо preview используется изменение разрешённой заметки. Не нужно считать появление маршрута сохранением данных: результатом сейчас является только принятие или отклонение формы.
Ошибочные варианты
Пустое название после trim должно дать 400. Название длиной 121 символ также отклоняется. Два поля title не принимаются независимо от того, совпадают ли их значения. А обычная кавычка внутри короткого названия должна остаться данными.
Последний случай полезен для понимания. Запрет всех кавычек сделал бы часть корректных названий невозможной и всё равно не заменил бы параметризацию SQL. Аналогично удаление угловых скобок изменяло бы текст, но не установило бы безопасный договор для ссылки или JavaScript.
Правила ввода должны соответствовать назначению поля. Номер заметки имеет числовой маршрут; имя пользователя получит ограниченный идентификатор; текст заметки остаётся строкой. Один фильтр с длинным списком запрещённых символов плохо выражает эти разные требования. Этот подход к приёмным правилам описан в рекомендациях OWASP по проверке данных.
Предел размера запроса
Ограничение длины строки применяется после чтения тела. Поэтому оно не заменяет предел всего HTTP-запроса. В начальном Flask-примере установлен MAX_CONTENT_LENGTH 16384 байта. Запрос большего размера должен получить отказ ещё на этапе обработки тела.
Этот предел относится к сумме формы и её служебной упаковки. Нельзя обещать посетителю 16384 байта полезного текста только из-за такого значения. Когда появятся изображения, общий предел увеличится, а размер текстовых полей останется отдельным. Настройки Flask позволяют ограничивать разные ресурсы, но их значения определяются нашим договором.
Кроме размера важны время чтения и количество параллельных обращений. Их не решает функция read_note_form. Прокси и сервер приложения имеют собственные пределы; в поздних уроках рассмотрим распределение ответственности. Проверка одного поля не должна называться защитой от любой нагрузки.
Отказ без лишних деталей
Посетителю достаточно понять, что форма не соответствует договору. Не возвращайте трассировку стека или исходное тело при ошибке. В интерфейсе можно подсветить поле и объяснить допустимую длину; внутренний журнал должен сообщать категорию нарушения без копии всего текста.
Ошибка 400 отличается от 404 неизвестного объекта и от 403 недействительного CSRF. Такое разделение помогает читателю разобрать механизм. При этом сообщение не обязано раскрывать, принадлежит ли чужая заметка определённому человеку: политика доступа будет задавать другой договор.
Сначала проверьте допустимый короткий текст, затем отсутствие поля, превышение длины и повтор. Если разрешённый текст неожиданно меняется, изучите нормализацию. Если лишнее поле проходит, проверьте набор allowed. Ожидаемый результат должен объясняться конкретным условием функции.
Значение после отказа
Представим название из 120 символов и такой же текст с одним дополнительным символом. Первый вариант должен пройти наше условие, второй — получить отказ. Обрезание второго до 120 изменило бы данные и скрыло причину. Для названия продукта иногда допустимо обрезать отображение карточки, но это другой слой, который не должен молча менять сохранённую строку.
Также проверьте текст с переводами строк и внешними пробелами. Название нормализуется по выбранному правилу, основной текст сохраняется. Это позволяет увидеть, что trim относится к конкретному полю. Привычка применять одно преобразование ко всему словарю могла бы повредить пароль в следующем уроке.
Кодировка тела и длина поля тоже различаются. Кириллица может занимать несколько байтов на символ, поэтому ограничение HTTP-тела и len строки имеют разные единицы. В интерфейсе полезно назвать именно пользовательский предел поля, а технический предел запроса оставить частью серверного договора.
Теперь сервер принимает данные по явным правилам. Следующий урок покажет, как принятая и сохранённая строка становится текстом страницы, сохраняя своё содержание и не превращаясь в HTML-разметку.