FastAPI: как запретить читать чужой ресурс по известному id

Пользователь вошёл в приложение и меняет номер заказа в URL. Сервер возвращает чужие данные, потому что маршрут проверяет только наличие авторизации. Вход подтверждает личность, но не разрешает чтение любого id. Каждый ресурс должен пройти отдельную проверку доступа.

Личность берите из проверенной зависимости

Не принимайте owner_id из query или тела как подтверждение прав. Клиент способен подставить любой номер. Маршрут получает текущего пользователя из доверенной зависимости, загружает запись и сравнивает владельца с этим пользователем. В базе удобнее сразу выбирать запись по id и owner_id, чтобы запрещённая запись не дошла до дальнейшей обработки.

В примере для отсутствующей и чужой записи используется 404. Это позволяет не раскрывать факт существования чужого объекта. В продукте допустим и 403, когда существование ресурса открыто, но операция запрещена. Выберите правило для интерфейса и применяйте одинаково ко всем маршрутам ресурса.

Проверяем доступ с двумя пользователями

Аутентификация в тесте намеренно подменена через dependency_overrides: мы проверяем именно владение. Исходная зависимость запрещает запросы и служит безопасной точкой подключения настоящей проверки токена. Нужны FastAPI и HTTPX.

Сохраните весь блок в файл и запустите Python 3.11+ в отдельном окружении с пакетами fastapi, httpx. Для моделей используется Pydantic v2; отдельный сервер для TestClient не нужен.

from typing import Annotated
from fastapi import FastAPI, Depends, HTTPException
from fastapi.testclient import TestClient

app = FastAPI()
records = {10: {"id": 10, "owner_id": 1, "text": "private"}}
def current_user():
    raise HTTPException(401, "authentication required")

@app.get("/notes/{note_id}")
def note(note_id: int, user_id: Annotated[int, Depends(current_user)]):
    record = records.get(note_id)
    if record is None or record["owner_id"] != user_id:
        raise HTTPException(404, "note not found")
    return {"id": record["id"], "text": record["text"]}

try:
    with TestClient(app) as client:
        assert client.get("/notes/10").status_code == 401
        app.dependency_overrides[current_user] = lambda: 1
        assert client.get("/notes/10").json() == {"id": 10, "text": "private"}
        app.dependency_overrides[current_user] = lambda: 2
        assert client.get("/notes/10").status_code == 404
        assert client.get("/notes/999").status_code == 404
        assert client.get("/notes/10?owner_id=1").status_code == 404
finally:
    app.dependency_overrides.clear()
print("owner allowed; another user and forged query owner denied")

Почему модель ответа не решает эту проблему

Даже минимальный публичный ответ может содержать чужую заметку, сумму заказа или адрес. Исключение password_hash из JSON полезно, но не заменяет право читать саму запись. Поэтому тесты доступа должны проверять разных пользователей, а тесты ответа — список открытых полей. Эти проверки дополняют друг друга и обнаруживают разные дефекты.

Проверьте не только GET. Чужой id может попасть в PATCH, DELETE, экспорт, вложенные файлы и список связанных объектов. Вынесите загрузку доступного ресурса в общую зависимость, если правила совпадают. Но оставьте различия между правом читать и правом редактировать явными: роль наблюдателя не должна случайно получить запись.

Как переносить правило в SQL

Для личного ресурса запрос WHERE id = :id AND owner_id = :current_user позволяет одинаково обработать отсутствие и чужое владение. Для команд изменения добавьте защиту от изменения владельца между чтением и записью, если такое изменение возможно. Условное UPDATE с проверкой числа изменённых строк часто выражает правило яснее отдельного чтения.

Матрица теста из двух пользователей и двух id — хороший минимум для проверки горизонтального доступа. Добавьте общую команду или администратора только при наличии такого правила в продукте. Не расширяйте разрешение по случайному признаку вроде совпадения домена почты: права должны следовать явной модели доступа.

Следующая практика

Продолжите с открытым уроком о текущем пользователе как зависимости. Когда маршрут получает личность из одного проверенного места, добавьте собственный тест двух пользователей и убедитесь, что чужой id не открывает запись. Перейдите к открытому уроку current user.

В бесплатном курсе FastAPI работа с текущим пользователем входит в проект вместе с базой и авторизацией. После базовых шагов оставьте свой тест владельца рядом с каждым методом доступа к записи. Откройте программу курса.

Продолжить на практике