FastAPI Backend Development Bootcamp · Lekcja

Ograniczanie 10 najważniejszych zagrożeń bezpieczeństwa API według OWASP

Mapuj typowe zagrożenia API na konkretne zabezpieczenia FastAPI dotyczące błędnej autoryzacji, BOLA i masowego przypisywania danych.

Lekcja 1 z 413 kroki

Ograniczanie 10 najważniejszych zagrożeń bezpieczeństwa API według OWASP to bezpłatna lekcja FastAPI Backend Development Bootcamp na CoddyKit. To lekcja 1 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej FastAPI Backend Development Bootcamp, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs FastAPI Backend Development Bootcamp zawiera 4 lekcji w sumie.

Części tej lekcji nie zostały jeszcze przetłumaczone i są wyświetlane po angielsku.

Why OWASP API Security Top 10 Matters

The OWASP API Security Top 10 is a curated list of the most critical security risks facing modern APIs. Unlike the classic OWASP Top 10 for web apps, this list focuses specifically on API attack surfaces — including authentication abuse, excessive data exposure, and mass assignment vulnerabilities.

FastAPI is a powerful framework, but it does not make your API secure by default. You must deliberately apply defenses at every layer: routing, validation, authentication, and serialization.

In this lesson we focus on three high-impact categories:

  • API1 — Broken Object Level Authorization (BOLA)
  • API2 — Broken Authentication
  • API6 — Mass Assignment

Each has a distinct attack pattern and a concrete FastAPI mitigation you can apply today.

Broken Authentication: The Attack Surface

Broken Authentication (API2) occurs when an API fails to properly verify that a caller is who they claim to be. Common failure modes include:

  • Accepting expired or tampered JWTs without signature verification
  • Using weak or predictable secrets for token signing
  • Not enforcing token expiry (exp claim)
  • Allowing unlimited login attempts (no rate limiting)

In FastAPI, the most reliable pattern is to validate JWTs with a library like python-jose or PyJWT on every protected route — using a dependency injected via Depends().

The dependency approach centralises auth logic so you cannot accidentally forget it on a new route.

Implementing a JWT Auth Dependency

Below is a production-style JWT dependency for FastAPI. It verifies the token signature, checks the exp claim, and raises a 401 on any failure. Inject it into every route that requires authentication.

Key hardening points:

  • algorithms=[ALGORITHM] — explicitly whitelist only HS256 (or RS256); never pass algorithms=None
  • The secret must come from an environment variable, never hardcoded
  • A missing or malformed sub claim is treated as an invalid token
import os
from datetime import datetime, timezone
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt

SECRET_KEY = os.environ["JWT_SECRET_KEY"]  # never hardcode
ALGORITHM = "HS256"

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")


def get_current_user_id(token: str = Depends(oauth2_scheme)) -> int:
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user_id: str = payload.get("sub")
        if user_id is None:
            raise credentials_exception
        return int(user_id)
    except JWTError:
        raise credentials_exception

BOLA: Broken Object Level Authorization

BOLA (API1) is the #1 API vulnerability worldwide. It happens when a caller can access any object simply by guessing its ID — because the server never checks ownership.

A classic vulnerable pattern:

GET /orders/9871

If the server fetches the order by order_id alone and returns it, any authenticated user can read any order in the database just by incrementing the ID.

The fix is always the same: after fetching the resource, compare the resource owner against the authenticated caller. If they do not match, return 403 Forbidden — not 404, which would leak existence information in some contexts.

Fixing BOLA in a FastAPI Route

The pattern: fetch the object, then assert ownership. Never skip the ownership check because you think the ID is hard to guess — UUIDs are not a security control.

Notice that get_current_user_id from the previous scene is injected via Depends(). The ownership assertion is a single if statement, but it is the most important line in the function.

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import get_current_user_id
from app.db import get_db
from app.models import Order

router = APIRouter()


@router.get("/orders/{order_id}")
async def get_order(
    order_id: int,
    current_user_id: int = Depends(get_current_user_id),
    db: AsyncSession = Depends(get_db),
):
    order = await db.get(Order, order_id)
    if order is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found")

    # BOLA fix: verify the caller owns this resource
    if order.user_id != current_user_id:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")

    return order

Writing a Reusable BOLA Guard

When your API has dozens of resource endpoints, repeating the ownership check inline is error-prone. Extract it into a reusable helper that raises an exception automatically. This reduces the chance of a developer forgetting the check on a new route.

The helper is generic enough to work with any SQLAlchemy model that has a user_id attribute. For more complex ownership rules (e.g., team-based access), extend this function with role/permission queries.

from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession


async def get_owned_resource(model_class, resource_id: int, user_id: int, db: AsyncSession):
    """
    Fetch a resource and assert ownership in one call.
    Raises 404 if not found, 403 if the caller does not own it.
    """
    resource = await db.get(model_class, resource_id)
    if resource is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"{model_class.__name__} not found",
        )
    if resource.user_id != user_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="You do not have permission to access this resource",
        )
    return resource

Mass Assignment: The Attack

Mass Assignment (API6) occurs when an API blindly maps request body fields onto a database model, allowing an attacker to set fields that were never meant to be user-controlled.

Classic example: a user updates their profile and includes "is_admin": true or "balance": 99999 in the JSON body. If the server does user.__dict__.update(request_data) without filtering, those fields are written to the database.

In FastAPI, Pydantic input schemas are your primary defence. They act as an explicit allowlist: only the fields declared in the schema can be accepted from the client. Any extra field is silently ignored (or rejected, depending on configuration).

Separating Input and Output Schemas

The most important pattern for preventing mass assignment is to use separate Pydantic models for input and output:

  • Input schema — only the fields a user is allowed to set
  • Output schema — all fields safe to return to the caller
  • DB model — the full record, including sensitive fields like is_admin, hashed_password, etc.

Never use the same schema for both input and output if they have different security requirements.

from pydantic import BaseModel, EmailStr
from typing import Optional


# What the client is ALLOWED to send when updating a profile
class UserUpdateInput(BaseModel):
    display_name: Optional[str] = None
    bio: Optional[str] = None
    email: Optional[EmailStr] = None
    # NOTE: is_admin, balance, role, hashed_password are NOT here


# What we return to the client (read-only fields visible but not settable)
class UserPublicOutput(BaseModel):
    id: int
    display_name: str
    email: EmailStr
    is_admin: bool

    model_config = {"from_attributes": True}

Applying Input Schema in an Update Route

With the separate schema in place, the update route uses model_dump(exclude_unset=True) to get only the fields the client actually sent. These are then applied to the database model one by one — never via a bulk __dict__ update.

exclude_unset=True is critical for PATCH semantics: it ensures that a missing field means "do not change" rather than "set to null".

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import get_current_user_id
from app.db import get_db
from app.models import User
from app.schemas import UserUpdateInput, UserPublicOutput

router = APIRouter()


@router.patch("/users/me", response_model=UserPublicOutput)
async def update_profile(
    payload: UserUpdateInput,
    current_user_id: int = Depends(get_current_user_id),
    db: AsyncSession = Depends(get_db),
):
    user = await db.get(User, current_user_id)
    if user is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")

    # Only apply fields the client actually provided
    update_data = payload.model_dump(exclude_unset=True)
    for field, value in update_data.items():
        setattr(user, field, value)

    await db.commit()
    await db.refresh(user)
    return user

Enforcing Extra Field Rejection

By default, Pydantic v2 silently ignores unknown fields. For security-sensitive schemas, you can configure the model to raise a validation error if an unexpected field is sent. This makes mass assignment attempts visible in logs and prevents silent data loss from typos.

Set model_config = ConfigDict(extra='forbid') on your input schemas. Your API will return 422 Unprocessable Entity if the client sends any field not declared in the schema.

from pydantic import BaseModel, EmailStr, ConfigDict
from typing import Optional


class StrictUserUpdateInput(BaseModel):
    model_config = ConfigDict(extra="forbid")  # reject unknown fields

    display_name: Optional[str] = None
    bio: Optional[str] = None
    email: Optional[EmailStr] = None


# Demonstration (runs standalone)
if __name__ == "__main__":
    import json
    from pydantic import ValidationError

    # Valid input
    valid = StrictUserUpdateInput(display_name="Alice")
    print("Valid:", valid.model_dump(exclude_unset=True))

    # Attacker tries to escalate privileges
    try:
        evil = StrictUserUpdateInput(display_name="Alice", is_admin=True)
    except ValidationError as e:
        errors = json.loads(e.json())
        print("Blocked:", errors[0]["type"], "-", errors[0]["loc"])

Combining All Three Defenses in One Flow

The three defenses work together as layers. Here is how they compose in a single document-update endpoint:

  1. Auth dependency — verifies the JWT and extracts current_user_id (defeats Broken Authentication)
  2. BOLA check — fetches the document and asserts doc.owner_id == current_user_id (defeats BOLA)
  3. Input schema with extra='forbid' — only allows title and content to be set (defeats Mass Assignment)

Each layer is independent. If you remove any one of them, the other two still provide partial protection — but all three are required for full coverage.

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, ConfigDict
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import get_current_user_id
from app.db import get_db
from app.models import Document

router = APIRouter()


class DocumentUpdateInput(BaseModel):
    model_config = ConfigDict(extra="forbid")
    title: Optional[str] = None
    content: Optional[str] = None


@router.patch("/documents/{doc_id}")
async def update_document(
    doc_id: int,
    payload: DocumentUpdateInput,                          # Layer 3: mass assignment guard
    current_user_id: int = Depends(get_current_user_id),  # Layer 1: auth
    db: AsyncSession = Depends(get_db),
):
    doc = await db.get(Document, doc_id)
    if doc is None:
        raise HTTPException(status_code=404, detail="Document not found")

    if doc.owner_id != current_user_id:                   # Layer 2: BOLA guard
        raise HTTPException(status_code=403, detail="Access denied")

    for field, value in payload.model_dump(exclude_unset=True).items():
        setattr(doc, field, value)

    await db.commit()
    await db.refresh(doc)
    return doc

Knowledge Check: Preventing BOLA

A FastAPI endpoint retrieves an invoice by its ID and returns it to the caller. The endpoint already requires a valid JWT. Which additional step is essential to prevent a BOLA (Broken Object Level Authorization) attack?

Lesson Recap: Three Defenses, One Secure API

In this lesson you mapped three OWASP API Security Top 10 threats to concrete FastAPI defenses:

  • Broken Authentication (API2) — centralise JWT validation in a Depends() dependency; whitelist the signing algorithm; load secrets from environment variables.
  • BOLA (API1) — after fetching any resource, always assert resource.owner_id == current_user_id. Return 403 on mismatch. Extract the check into a reusable helper to prevent omissions.
  • Mass Assignment (API6) — use separate input and output Pydantic schemas; set extra='forbid' on input schemas; apply updates field-by-field with exclude_unset=True.

These three patterns are independent and composable. Applied together via FastAPI's dependency injection system, they eliminate the most common API attack vectors at the framework level — before a single line of business logic runs.

Bezpłatny start

Ucz się FastAPI Backend Development Bootcamp dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
21
Lekcje
84

Często zadawane pytania

Czy lekcja „Ograniczanie 10 najważniejszych zagrożeń bezpieczeństwa API według OWASP” jest bezpłatna?

Tak — pełny tekst „Ograniczanie 10 najważniejszych zagrożeń bezpieczeństwa API według OWASP” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu FastAPI Backend Development Bootcamp, przejdź na CoddyKit PRO. Kurs FastAPI Backend Development Bootcamp zawiera 4 lekcji w sumie.

Co nauczysz się w „Ograniczanie 10 najważniejszych zagrożeń bezpieczeństwa API według OWASP”?

Mapuj typowe zagrożenia API na konkretne zabezpieczenia FastAPI dotyczące błędnej autoryzacji, BOLA i masowego przypisywania danych. Ćwiczysz FastAPI Backend Development Bootcamp z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć FastAPI Backend Development Bootcamp?

Nie wymagamy żadnego doświadczenia. FastAPI Backend Development Bootcamp w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 1 z 4.

Ile czasu zajmuje lekcja „Ograniczanie 10 najważniejszych zagrożeń bezpieczeństwa API według OWASP”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji FastAPI Backend Development Bootcamp?

Tak. Każda lekcja FastAPI Backend Development Bootcamp zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Ograniczanie 10 najważniejszych zagrożeń bezpieczeństwa API według OWASP
  2. Ograniczanie częstotliwości żądań i ochrona przed nadużyciami botów
  3. Zarządzanie sekretami i rotacja kluczy
  4. CORS, CSP i zasady bezpiecznych nagłówków
← Powrót do FastAPI Backend Development Bootcamp