| |

REST API 24 ๐ŸŒ Security Hardening (OWASP API Top 10)

โฑ๏ธ Estimated read: 17 min | |

API security is not a feature that can be bolted on after the endpoints are built. The OWASP API Security Top 10 exists because the failures that actually cause breaches are structural: authorization checks that were never written, authentication tokens that were never verified, and resource limits that were never enforced . The 2023 edition reflects a decisive shift in the threat landscape. Injection, which dominated web application lists for years, does not appear as a standalone API category. Authorization failures occupy three of the top five positions, because APIs expose object identifiers directly and the gap between “who is calling” and “what they are allowed to access” is where most real incidents occur .

This chapter covers the 2023 list in depth, with the implementation patterns that mitigate each category. It examines the authorization failures at the top, the authentication weaknesses that follow, the resource and business-flow risks that the 2023 revision added, and the configuration and inventory problems that let forgotten endpoints become entry points.

Key point: The most dangerous API vulnerability is not a clever exploit. It is an endpoint that authenticates the caller but never checks whether that caller is authorized for the specific object being requested .


Why the OWASP API Top 10 exists

The authentication-authorization gap problem. Modern frameworks make authentication trivial. A JWT middleware validates the token, attaches a user ID to the request, and the handler proceeds. The assumption that an authenticated request is a safe request is the root cause of the most exploited API vulnerability . BOLA exists in the space between knowing who is calling and verifying what they can access. The fix is not a better authentication library; it is an authorization check on every object access.

The structural omission problem. When authorization is implemented inside individual endpoint handlers, every new endpoint depends on a developer remembering to add the correct check. Over time, endpoints are added quickly, code is refactored, teams change, and APIs evolve. Even in well-maintained systems, some endpoints will miss checks. BOLA is a structural outcome of this pattern, not a sign of incompetence . Centralized enforcementโ€”at the middleware layer, at the data access layer, or through a policy engineโ€”is the architectural fix.

The automation abuse problem. The 2023 list added “Unrestricted Access to Sensitive Business Flows” because some attacks do not exploit a bug. The API works exactly as designed. The problem is that a bot can use the designed flow thousands of times per second to scalp inventory, farm coupons, or create fake accounts . Rate limiting alone does not solve this because the abuse is indistinguishable from legitimate traffic at the request level. Behavioral controlsโ€”device fingerprinting, CAPTCHA, anomaly detectionโ€”are required.

The trusted third-party problem. “Unsafe Consumption of APIs” flips the usual perspective. Instead of asking how an attacker compromises your API, it asks what happens when your API trusts a third-party response too much. If an integration partner is compromised and your API ingests their data without validation, the partner’s breach becomes yours . Every upstream response must be treated as untrusted input.


a. Broken Object Level Authorization (API1) and Broken Function Level Authorization (API5)

BOLA (formerly IDOR) has held the top position since the list’s first edition in 2019. The vulnerability is simple to describe and devastating at scale: an endpoint accepts an object ID from the request and returns the object without verifying that the authenticated user owns or is permitted to access it . An attacker changes /api/orders/1043 to /api/orders/1044 and reads someone else’s order.

The mitigation is not a conditional in the handler. It is scoping the database query to the authenticated user so that authorization is part of the data access, not a separate check that can be forgotten.

# VULNERABLE: fetches the object, then checks ownership
# The object exists in memory before authorization runs
order = Order.objects.get(id=order_id)
if order.user_id != request.user.id:
    raise PermissionDenied
return order

# SECURE: the query itself enforces ownership
# Unauthorized objects are never loaded
from django.shortcuts import get_object_or_404
order = get_object_or_404(Order, id=order_id, user=request.user)
return order

The secure version returns a 404 for unauthorized access rather than a 403. This avoids confirming that the object exists, which an attacker can use to enumerate valid IDs . The same principle applies in any ORM or query builder: the WHERE clause includes the ownership condition.

Broken Function Level Authorization (API5) is the sibling problem at the endpoint level rather than the object level. An admin-only endpoint is reachable by a regular user because the server checks that a session exists but not what role it holds . The fix is centralized role enforcementโ€”middleware or a policy engineโ€”rather than per-route conditionals that new endpoints can forget to include.

// VULNERABLE: only checks authentication
app.delete('/api/admin/users/:id', authenticate, deleteUser);

// SECURE: checks role at the middleware layer
const requireAdmin = (req, res, next) => {
  if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin required' });
  next();
};
app.delete('/api/admin/users/:id', authenticate, requireAdmin, deleteUser);

b. Broken Authentication (API2) and Broken Object Property Level Authorization (API3)

Broken Authentication covers the failures that let an attacker assume another user’s identity: JWTs whose signatures are not verified server-side, tokens that never expire, credential stuffing that succeeds because login endpoints have no rate limiting, and API keys transmitted in URLs where they are logged . The common thread is that the API trusts a request it should not.

The mitigation is to treat authentication as its own hardened subsystem. Validate JWT signatures and expiration on every request. Reject alg: none. Enforce rate limiting and account lockout on every credential-based endpoint. Use short-lived access tokens with refresh rotation .

Broken Object Property Level Authorization (API3) is new in the 2023 list, merging the 2019 categories of Excessive Data Exposure and Mass Assignment. The vulnerability exists at the property level: an API can allow users to read or write fields they should not access even when object-level authorization is correct . On the read side, a GET /api/users/{id} endpoint that serializes the entire database row leaks passwordHash, internal flags, and role fields. On the write side, a PATCH /api/users/{id} endpoint that blindly binds the request body to the model allows an attacker to set "role": "admin" alongside the fields the form actually exposed.

# VULNERABLE: mass assignment โ€” any field in the body is persisted
@app.patch("/users/{user_id}")
async def update_user(user_id: str, body: dict):
    user = await db.get(User, user_id)
    for key, value in body.items():
        setattr(user, key, value)  # Attacker sends {"role": "admin"}
    await db.commit()

# SECURE: explicit allow-list of writable fields
class UserUpdate(BaseModel):
    name: str | None = None
    email: str | None = None
    # role is NOT included; it cannot be set through this endpoint

@app.patch("/users/{user_id}")
async def update_user(user_id: str, body: UserUpdate):
    user = await db.get(User, user_id, owner=request.user)
    for key, value in body.model_dump(exclude_unset=True).items():
        setattr(user, key, value)
    await db.commit()

The same allow-list discipline applies to responses. Return only the fields the endpoint’s contract defines, not whatever the ORM happens to serialize .


c. Unrestricted Resource Consumption (API4), Sensitive Business Flows (API6), and the Remaining Risks

Unrestricted Resource Consumption replaced the narrower “Lack of Resources and Rate Limiting” category. The renaming reflects the fact that a basic requests-per-minute limit does not cover the cost-per-request dimension . A GraphQL query nested nine levels deep generates thousands of database calls behind one HTTP request that a rate limiter counts as one hit. A REST endpoint with an unbounded ?limit= parameter can exhaust memory with a single request. A bulk export endpoint with no cap on array length can process millions of records in one call .

// VULNERABLE: client controls the limit
app.get('/api/users', async (req, res) => {
  const users = await User.find().limit(req.query.limit);  // ?limit=1000000
});

// SECURE: server enforces a maximum
app.get('/api/users', async (req, res) => {
  const limit = Math.min(parseInt(req.query.limit) || 20, 100);
  const users = await User.find().limit(limit);
});

// Payload size limit at the middleware layer
app.use(express.json({ limit: '1mb' }));

Unrestricted Access to Sensitive Business Flows (API6) is new in 2023 and addresses automation abuse of legitimate workflows. The API is functioning correctly; the problem is that a script can use it at a scale that harms the businessโ€”scalping inventory, farming coupons, or creating fake accounts in bulk . Defenses are behavioral rather than structural: CAPTCHA on high-value flows, device fingerprinting, and anomaly detection that distinguishes a real customer from a scripted one.

The remaining categories cover configuration and inventory. Security Misconfiguration (API8) includes verbose error traces that leak stack frames, wildcard CORS headers on authenticated APIs, and default credentials left in place . Improper Inventory Management (API9) is the problem of forgotten endpointsโ€”staging routes, deprecated API versions, and debug endpoints that remain reachable because no one remembers they exist . Server Side Request Forgery (API7) occurs when an API fetches a user-supplied URL without validating the destination, allowing an attacker to reach internal services or cloud metadata endpoints. Unsafe Consumption of APIs (API10) is the risk of trusting third-party responses without validation .


Complete Example Session

# ============================================
# PART 1: BOLA โ€” SCOPED QUERY PATTERN
# ============================================
# The ownership condition is part of the query, not a separate check
from fastapi import HTTPException

async def get_order(order_id: str, user: User, db: AsyncSession):
    # The WHERE clause includes user ownership
    result = await db.execute(
        select(Order).where(Order.id == order_id, Order.user_id == user.id)
    )
    order = result.scalar_one_or_none()
    if not order:
        # 404 for both "not found" and "not yours" โ€” no enumeration signal
        raise HTTPException(status_code=404)
    return order


# ============================================
# PART 2: FUNCTION-LEVEL AUTHORIZATION
# ============================================
# Centralized role check, not per-route conditionals
from fastapi import Depends

def require_admin(user: User = Depends(get_current_user)):
    if user.role != "admin":
        raise HTTPException(status_code=403)
    return user

@app.delete("/admin/users/{user_id}")
async def delete_user(user_id: str, admin: User = Depends(require_admin)):
    # Only reached if require_admin passes
    await db.delete(User, user_id)


# ============================================
# PART 3: MASS ASSIGNMENT PREVENTION
# ============================================
# Pydantic model defines the allow-list of writable fields
from pydantic import BaseModel

class UserUpdate(BaseModel):
    name: str | None = None
    email: str | None = None
    # role intentionally excluded

@app.patch("/users/me")
async def update_profile(body: UserUpdate, user: User = Depends(get_current_user)):
    # Only fields in UserUpdate can be set
    for key, value in body.model_dump(exclude_unset=True).items():
        setattr(user, key, value)
    await db.commit()


# ============================================
# PART 4: RESPONSE SHAPING
# ============================================
# Explicit response model prevents excessive data exposure
class UserResponse(BaseModel):
    id: str
    name: str
    email: str
    # password_hash, internal_flags, role are NOT included

@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: str, user: User = Depends(get_current_user)):
    return await get_user_or_404(user_id, user)


# ============================================
# PART 5: RESOURCE LIMITS
# ============================================
# Server-enforced pagination and payload limits
from fastapi import Query

@app.get("/orders")
async def list_orders(
    limit: int = Query(default=20, le=100),  # Max 100
    offset: int = Query(default=0, ge=0)
):
    # limit is guaranteed โ‰ค 100 by Query validation
    return await db.fetch_orders(limit=limit, offset=offset)

# Body size limit at the middleware layer
# app.add_middleware(BodySizeLimitMiddleware, max_size=1_000_000)


# ============================================
# PART 6: AUTHENTICATION HARDENING
# ============================================
# JWT validation: signature, expiry, algorithm
from jose import jwt, JWTError

def verify_token(token: str) -> dict:
    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=["HS256"],  # Reject "none" and weak algorithms
            options={"require_exp": True}
        )
        return payload
    except JWTError:
        raise HTTPException(status_code=401)


# ============================================
# PART 7: RATE LIMITING
# ============================================
# Per-user and per-endpoint limits, not just global
from slowapi import Limiter
limiter = Limiter(key_func=get_user_id)

@app.post("/login")
@limiter.limit("5/minute")  # Credential stuffing protection
async def login(credentials: LoginRequest):
    return await authenticate(credentials)


# ============================================
# PART 8: SSRF PREVENTION
# ============================================
# Validate URLs before fetching
from urllib.parse import urlparse

ALLOWED_HOSTS = {"api.trusted.com", "cdn.example.com"}

def validate_url(url: str) -> bool:
    parsed = urlparse(url)
    # Reject non-HTTPS, private IPs, and non-allowlisted hosts
    if parsed.scheme != "https":
        return False
    if parsed.hostname not in ALLOWED_HOSTS:
        return False
    return True

@app.post("/fetch")
async def fetch_remote(body: FetchRequest):
    if not validate_url(body.url):
        raise HTTPException(status_code=400, detail="Invalid URL")
    return await http_client.get(body.url)


# ============================================
# PART 9: SECURITY CONFIGURATION
# ============================================
# CORS: explicit origins, not wildcard
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],  # No "*"
    allow_credentials=True,
    allow_methods=["GET", "POST", "PATCH", "DELETE"],
)

# Error responses: no stack traces in production
@app.exception_handler(Exception)
async def generic_handler(request, exc):
    return JSONResponse(status_code=500, content={"error": "Internal error"})


# ============================================
# PART 10: INVENTORY AND THIRD-PARTY VALIDATION
# ============================================
# Every endpoint is documented in OpenAPI (no shadow APIs)
# Third-party responses are validated against schema
import jsonschema

def validate_partner_response(data: dict):
    schema = {
        "type": "object",
        "required": ["id", "status"],
        "properties": {
            "id": {"type": "string"},
            "status": {"type": "string", "enum": ["active", "inactive"]}
        }
    }
    jsonschema.validate(data, schema)  # Raises on invalid
    return data

The ten parts trace the implementation patterns for the highest-priority categories: scoped queries for BOLA, centralized role checks for function-level authorization, allow-lists for mass assignment and response shaping, server-enforced limits for resource consumption, JWT validation and rate limiting for authentication, URL allow-lists for SSRF, explicit CORS and error handling for misconfiguration, and schema validation for third-party responses.


Quick Reference

The OWASP API Top 10 (2023)

RiskCore FailurePrimary Mitigation
API1: BOLAObject accessed without ownership checkScope queries to authenticated user
API2: Broken AuthTokens unverified, no rate limitsValidate signature/expiry, rate limit
API3: Property AuthMass assignment, data exposureAllow-lists for request and response
API4: Resource UseNo limits on size or costServer-enforced caps, rate limiting
API5: Function AuthAdmin endpoints open to usersCentralized role middleware
API6: Business FlowAutomation abuse of legitimate flowsCAPTCHA, fingerprinting, anomaly detection
API7: SSRFUser-supplied URL fetchedURL allow-list, block private IPs
API8: MisconfigurationWildcard CORS, verbose errorsExplicit origins, generic errors
API9: InventoryForgotten endpoints reachableEndpoint inventory, deprecation
API10: Unsafe ConsumptionThird-party data trustedSchema validation on upstream

Authorization Models

ModelAnswersUse For
OwnershipDoes this user own this object?Personal resources
RBACWhat actions can this role perform?Function-level checks
ABACGiven attributes, is access allowed?Multi-tenant, delegated access

Essential Middleware

ControlPurposeImplementation
Auth validationVerify identityJWT middleware
Rate limiterPrevent brute force and DoSPer-user, per-endpoint
Body size limitPrevent memory exhaustion1 MB default
CORSControl browser accessExplicit origins

Best Practices

โœ… Do This:

# Scope the query to the authenticated user
get_object_or_404(Order, id=order_id, user=request.user)              # โœ…
# Use explicit allow-lists for writable fields
class UserUpdate(BaseModel):
    name: str | None = None
    email: str | None = None                                            # โœ…
// Enforce role at the middleware layer
const requireAdmin = (req, res, next) => { if (req.user.role !== 'admin') return res.status(403); next(); } // โœ…
# Enforce server-side maximums on pagination
limit: int = Query(default=20, le=100)                                  # โœ…
# Validate JWT signature and algorithm
jwt.decode(token, SECRET, algorithms=["HS256"], options={"require_exp": True}) # โœ…
# Validate third-party responses against schema
jsonschema.validate(partner_data, PARTNER_SCHEMA)                       # โœ…

โŒ Don’t Do This:

# Fetch object, then check ownership (object loaded before auth)
order = Order.objects.get(id=order_id)
if order.user_id != request.user.id: raise PermissionDenied            # โŒ
# Blindly bind request body to model
for key, value in body.items(): setattr(user, key, value)              # โŒ
// Check authentication only, not role
app.delete('/admin/users/:id', authenticate, handler)                  # โŒ
# Trust client-supplied limit
User.find().limit(req.query.limit)                                     # โŒ
// Wildcard CORS on authenticated API
app.use(cors({ origin: '*' }))                                          # โŒ
# Fetch user-supplied URL without validation
await http.get(body.url)                                               # โŒ

Common Pitfalls

PitfallWhy It HappensFix
BOLA found in productionAuthorization in handler, forgotten on new endpointScope queries; centralize enforcement
Mass assignment succeedsRequest body bound directly to modelPydantic/schema allow-list
Admin endpoint openAuthentication checked, role notRole middleware on privileged routes
Resource exhaustionClient controls limit or body sizeServer-side maximums
SSRF via webhook URLURL fetched without destination validationAllow-list hosts, block private IPs
Shadow API exploitedForgotten staging endpoint reachableInventory, deprecation, gateway rules
Third-party breach becomes yoursPartner response trusted without validationSchema validation on all upstream data

Real-World Examples

1. Scoped Query for BOLA

get_object_or_404(Order, id=order_id, user=request.user)

2. Role Middleware

const requireAdmin = (req, res, next) => {
  if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin required' });
  next();
};

3. Allow-List Update Model

class UserUpdate(BaseModel):
    name: str | None = None
    email: str | None = None

4. Server-Enforced Pagination

limit: int = Query(default=20, le=100)

5. JWT Validation

jwt.decode(token, SECRET, algorithms=["HS256"], options={"require_exp": True})

6. Rate Limiting Login

@limiter.limit("5/minute")
async def login(...): ...

7. URL Allow-List for SSRF

ALLOWED_HOSTS = {"api.trusted.com"}
if urlparse(url).hostname not in ALLOWED_HOSTS: reject()

8. Explicit CORS

allow_origins=["https://app.example.com"]

9. Generic Error Response

return JSONResponse(status_code=500, content={"error": "Internal error"})

10. Schema Validation for Partner Data

jsonschema.validate(partner_data, PARTNER_SCHEMA)

Visual

The BOLA Gap

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  REQUEST: GET /api/orders/1044                               โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ Authentication  โ”‚  "User is Alice (id=1)"                โ”‚
โ”‚  โ”‚ middleware      โ”‚                                        โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ Handler         โ”‚                                        โ”‚
โ”‚  โ”‚                 โ”‚                                        โ”‚
โ”‚  โ”‚ Order.objects   โ”‚  WHERE id=1044                         โ”‚
โ”‚  โ”‚ .get(id=1044)   โ”‚  No ownership condition                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ Returns order   โ”‚  Order belongs to Bob (id=2)           โ”‚
โ”‚  โ”‚ 1044 to Alice   โ”‚  BOLA VULNERABILITY                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚                                                              โ”‚
โ”‚  FIX: WHERE id=1044 AND user_id=1                            โ”‚
โ”‚  The query itself enforces ownership.                        โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Three Authorization Layers

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  LAYER 1: AUTHENTICATION                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚ Who is calling?                         โ”‚                โ”‚
โ”‚  โ”‚ JWT signature, expiry, algorithm        โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚           โ”‚                                                  โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  LAYER 2: FUNCTION AUTHORIZATION                             โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚ Can this role access this endpoint?     โ”‚                โ”‚
โ”‚  โ”‚ Admin middleware, RBAC                  โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚           โ”‚                                                  โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  LAYER 3: OBJECT AUTHORIZATION                               โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚ Can this user access this object?       โ”‚                โ”‚
โ”‚  โ”‚ Scoped query, ownership condition       โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚           โ”‚                                                  โ”‚
โ”‚           โ–ผ                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                        โ”‚
โ”‚  โ”‚ Handler logic   โ”‚  All three layers passed               โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                        โ”‚
โ”‚                                                              โ”‚
โ”‚  Missing layer 3 = BOLA (API1).                              โ”‚
โ”‚  Missing layer 2 = Function-level auth (API5).               โ”‚
โ”‚  Missing layer 1 = Broken authentication (API2).             โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Mass Assignment Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  VULNERABLE                          SECURE                  โ”‚
โ”‚                                                              โ”‚
โ”‚  Request body:                       Request body:           โ”‚
โ”‚  {"name": "Alice",                   {"name": "Alice",       โ”‚
โ”‚   "email": "a@x.com",                "email": "a@x.com"}     โ”‚
โ”‚   "role": "admin"}                                           โ”‚
โ”‚           โ”‚                                  โ”‚               โ”‚
โ”‚           โ–ผ                                  โ–ผ               โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚
โ”‚  โ”‚ Bind all fields โ”‚                โ”‚ Pydantic model  โ”‚      โ”‚
โ”‚  โ”‚ to user model   โ”‚                โ”‚ only name,email โ”‚      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ”‚
โ”‚           โ–ผ                                  โ–ผ               โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚
โ”‚  โ”‚ user.role =     โ”‚                โ”‚ role field      โ”‚      โ”‚
โ”‚  โ”‚ "admin"         โ”‚                โ”‚ ignored         โ”‚      โ”‚
โ”‚  โ”‚                 โ”‚                โ”‚                 โ”‚      โ”‚
โ”‚  โ”‚ PRIVILEGE       โ”‚                โ”‚ No escalation   โ”‚      โ”‚
โ”‚  โ”‚ ESCALATION      โ”‚                โ”‚                 โ”‚      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ”‚
โ”‚                                                              โ”‚
โ”‚  The allow-list defines what can be written.                 โ”‚
โ”‚  Everything else is rejected or ignored.                     โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The 2023 List at a Glance

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  TOP 5 (AUTHORIZATION & AUTHENTICATION)                      โ”‚
โ”‚                                                              โ”‚
โ”‚  API1  BOLA              Object ownership not checked        โ”‚
โ”‚  API2  Broken Auth       Identity not verified correctly     โ”‚
โ”‚  API3  Property Auth     Field-level read/write not checked  โ”‚
โ”‚  API4  Resource Use      No limits on cost or size           โ”‚
โ”‚  API5  Function Auth     Role not checked per endpoint       โ”‚
โ”‚                                                              โ”‚
โ”‚  BOTTOM 5 (EMERGING & OPERATIONAL)                           โ”‚
โ”‚                                                              โ”‚
โ”‚  API6  Business Flows    Automation abuse of legitimate flow โ”‚
โ”‚  API7  SSRF              User URL fetched without validation โ”‚
โ”‚  API8  Misconfiguration  Insecure defaults, verbose errors   โ”‚
โ”‚  API9  Inventory         Forgotten endpoints still reachable โ”‚
โ”‚  API10 Unsafe Consumption Third-party data trusted blindly   โ”‚
โ”‚                                                              โ”‚
โ”‚  Authorization failures dominate.                            โ”‚
โ”‚  Injection is not a standalone API category.                 โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
OWASP API Top 10 edition2023 (current)
Top riskBOLA (API1), unchanged since 2019
Three authorization categoriesAPI1, API3, API5
BOLA fixScope query to authenticated user
Mass assignment fixExplicit allow-list for writable fields
Function auth fixCentralized role middleware
Resource fixServer-enforced caps, not client limits
SSRF fixURL allow-list, block private IPs
New in 2023API6 (business flows), API7 (SSRF), API10 (consumption)

Key takeaways:

  • BOLA is the top risk because authorization is easy to forget. The fix is not a conditional in the handler. It is scoping the query to the authenticated user so the object is never loaded without ownership being verified .
  • Mass assignment and excessive data exposure are the same bug in two directions. API3 exists because APIs do not control which fields a client can read or write. Explicit allow-lists for both request and response schemas close both directions .
  • Authentication is not authorization. A valid JWT proves who the caller is. It does not prove they can access the object or function they are requesting. Missing layer 2 or layer 3 produces API5 or API1, respectively.
  • Rate limiting alone does not stop resource consumption. A single request with an unbounded limit or a deeply nested GraphQL query can exhaust resources while counting as one hit against a per-minute limiter. Server-enforced maximums are required .
  • The 2023 list added categories for automation abuse and third-party trust. API6 addresses legitimate flows abused at scale; API10 addresses partner responses trusted without validation. Both reflect how APIs are used, not just how they are built .
  • Inventory is a security control. Forgotten staging endpoints, deprecated versions, and debug routes remain reachable because no one remembers they exist. API9 exists because you cannot secure what you do not know about .

Remember: The OWASP API Top 10 is not a checklist to complete once. It is a map of the structural failures that recur across APIs because the framework made authentication easy and authorization is still the developer’s responsibility. The single most valuable change is to move object authorization into the data access layerโ€”scope every query to the authenticated userโ€”so that a forgotten check in a handler cannot expose data. The second most valuable change is to define explicit schemas for every request and response so that mass assignment and data exposure are prevented by construction, not by memory. Everything else on the list is important, but these two patterns address the vulnerabilities that occupy the top three positions and cause the majority of real breaches.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!