Права на отдельные объекты
Приложение уже проверяет пароль и восстанавливает текущего пользователя из сессии. Теперь нужно связать эту личность с объектом. Boris может быть правильно авторизован для входа, но заметка 1 принадлежит Alice. Проверка «пользователь вошёл» сама по себе не отвечает на вопрос о доступе к ней.
Результат урока — выбор заметки по номеру и владельцу, а также такое же ограничение изменения. Это одна из центральных границ security-lab. Она должна действовать в любом маршруте, независимо от наличия ссылки в интерфейсе.
Личность и полномочие
Аутентификация устанавливает, какой пользователь отправил запрос. Авторизация определяет, разрешено ли ему действие с конкретным объектом. Эти решения могут опираться друг на друга, но не являются одним условием.
В нашей модели владельцем заметки является users.id. Сервер получает ID действующего пользователя из g.user после проверки сессии и auth_version. Не нужно спрашивать браузер, чью заметку он хочет считать своей: параметр owner_id в форме не устанавливает полномочие.
Скрытие ссылки на чужую заметку помогает оформлению, но не ограничивает HTTP-запрос. Пользователь знает адрес, может изменить число или сохранить прежнюю ссылку. Проверка должна находиться на пути к данным, а не только при построении списка.
Выбор разрешённого объекта
Ниже две функции заключительного приложения. Db получает соединение SQLite текущего запроса; g.user подготовлен обработчиком предыдущего урока.
def require_user():
if g.user is None:
abort(401, description="Требуется вход")
return g.user
def note_for_user(note_id):
user = require_user()
item = db().execute(
"SELECT * FROM notes WHERE id = ? AND owner_id = ?",
(note_id, user["id"]),
).fetchone()
if item is None:
abort(404, description="Заметка недоступна")
return item
Запрос включает оба условия. Правильная сессия Boris не меняет owner_id записи 1, поэтому выбор не вернёт её. Отсутствующий объект и чужой объект получают одинаковый публичный ответ 404. Это выбранная политика приложения, уменьшающая раскрытие существования чужих записей.
Такой ответ не обещает одинакового времени каждого запроса и не скрывает все сведения о базе. Он задаёт понятный договор содержимого. Главное требование — чужой текст не возвращается; детали политики ошибок обсуждаются отдельно.
HTML-маршрут теперь вызывает общий helper:
@app.get("/notes/<int:note_id>")
def note(note_id):
return render_template("note.html", note=note_for_user(note_id))
Он заменяет прежний публичный обработчик. Список также выбирает только owner_id текущего пользователя. Если закрыть одну заметку, но оставить общий JSON всех записей, требование первого урока всё ещё будет нарушено.
Изменение той же заметки
Проверка чтения не ограничивает запись автоматически. Для изменения используем условие владельца в UPDATE. Номер и содержание передаются параметрами.
user = require_user()
title, text = read_note_form(request.form)
cursor = db().execute(
"UPDATE notes SET title = ?, text = ? WHERE id = ? AND owner_id = ?",
(title, text, note_id, user["id"]),
)
if cursor.rowcount != 1:
abort(404, description="Заметка недоступна")
db().commit()
Это содержательная ветка POST-обработчика. Проверка подлинности формы появится следующим уроком, поэтому сейчас разбираем только право изменения. Полный исходник соединит оба требования до записи в базу.
Условие действует в самой операции. Даже если позже появится передача владельца или другая конкурентная операция, UPDATE не изменит строку с уже неподходящим owner_id. Предварительное чтение полезно для интерфейса, но не должно быть единственной защитой последующей записи.
Матрица наблюдений
Для Alice запрос заметки 1 ожидается успешным, для Boris — 404. Для анонимного запроса сначала действует требование входа. Затем аналогично проверяется изменение: содержимое Alice должно остаться прежним после попытки другого владельца.
| Отправитель | Объект | Чтение | Изменение |
|---|---|---|---|
| alice | 1 | Разрешено | Разрешено при корректной форме |
| boris | 1 | Недоступно | Недоступно |
| boris | 2 | Разрешено | Разрешено при корректной форме |
| Без входа | 1 | Требуется вход | Требуется вход |
Слова в таблице обозначают ожидания. В практическом отчёте дополнительно нужно сравнить запись базы до и после. Ответ 404 полезен, но если обработчик успел изменить данные перед отказом, поведение всё равно неверно.
Принцип проверки прав на каждое обращение описан в OWASP Authorization. Наш helper выражает его для конкретной модели владения. Более сложные роли и общие документы потребуют других правил, а не случайного добавления условия admin.
Устойчивость правила
В новом API используйте тот же note_for_user. Не создавайте отдельный «быстрый» маршрут с выбором только по ID ради удобства клиента. Формат JSON не уменьшает важность содержимого объекта.
Генерация случайного UUID вместо числа также не заменяет право. Она может затруднить угадывание, но утёкшая ссылка или журнал всё равно раскрывает идентификатор. Доступ определяется отношением отправителя к объекту, а не надеждой на неизвестность его номера.
При появлении администратора нужно отдельно определить его действия. Чтение всех заметок, отключение пользователя и подготовка базы не обязаны быть одинаковыми полномочиями. В маленьком стенде HTTP-роли администратора нет, что сохраняет правило простым.
Следующая граница
Даже разрешённый пользователь может отправить изменение без явного намерения. Браузер автоматически добавляет cookie к подходящему запросу. Поэтому право Alice на заметку ещё не доказывает, что форму отправила наша страница.
Список и новые операции
Предположим, что список карточек уже показывает только свои заметки, но его счётчик вычислен по всей таблице. Текст чужих записей не раскрывается, однако ответ всё равно сообщает лишние сведения. Количество, фильтры и сами карточки должны происходить из одного разрешённого множества.
Другой вариант — новый пакетный API принимает несколько номеров. Проверка права на один первый объект не разрешает остальные. Нужно определить политику целой операции: отказ при любом чужом объекте или явно ограниченный набор собственных результатов. Для нашей серии пакетной функции нет, поэтому её нельзя считать защищённой старым одиночным helper.
При передаче заметки другому владельцу изменится отношение в базе. Старый открытый экран может продолжать показывать уже раскрытый текст, но новый запрос должен учитывать текущее owner_id. Проверка только в момент входа не способна применить это изменение.
Также сохраняйте условия после восстановления копии. Если в снимке остались прежние владельцы, возврат базы изменяет правила конкретных объектов. Авторизация выполняется правильно относительно восстановленных данных, но это ещё не означает, что сами данные соответствуют актуальному решению.
Теперь сервер ограничивает объект по владельцу и применяет условие при записи. Следующий урок добавит CSRF-токен, сохранив авторизацию как самостоятельную обязательную проверку.