API Security Blueprint: Defending Microservices Against BOLA and BFLA Attacks

API Security Blueprint: Defending Microservices Against BOLA and BFLA Attacks

APIs are the connective tissue of modern software. They are also the primary attack surface. The OWASP API Security Top 10 places Broken Object Level Authorization (BOLA) and Broken Function Level Authorization (BFLA) at positions #1 and #5 respectively — and for good reason. These vulnerabilities are endemic, trivially exploitable, and catastrophically impactful.

In microservices architectures, where dozens of APIs communicate internally and externally, a single misconfigured authorization layer creates a blast radius that spans entire domains of sensitive data.


Understanding BOLA (Broken Object Level Authorization)

BOLA occurs when an API exposes object identifiers (IDs) in requests but fails to verify that the authenticated user has permission to access that specific object.

Vulnerable pattern:

GET /api/v1/invoices/84729
Authorization: Bearer <user_A_token>

If the server returns the invoice regardless of whether user_A owns invoice 84729, it is vulnerable to BOLA. An attacker simply iterates IDs:

import requests

headers = {"Authorization": f"Bearer {attacker_token}"}
for invoice_id in range(80000, 90000):
    r = requests.get(f"https://api.example.com/api/v1/invoices/{invoice_id}", headers=headers)
    if r.status_code == 200:
        exfiltrate(r.json())  # Every 200 is someone else's data

This is not a theoretical attack. BOLA was the root cause of the Optus (11M customer records), T-Mobile (2021), and dozens of fintech API breaches.


Implementing Robust Object-Level Authorization

Option 1: Database-level ownership checks (mandatory baseline)

Never trust the client-supplied ID alone. Always enforce ownership in your query:

# VULNERABLE
def get_invoice(invoice_id: int, current_user: User):
    return db.query(Invoice).filter(Invoice.id == invoice_id).first()

# SECURE — ownership enforced at query level
def get_invoice(invoice_id: int, current_user: User):
    invoice = (
        db.query(Invoice)
        .filter(Invoice.id == invoice_id, Invoice.owner_id == current_user.id)
        .first()
    )
    if not invoice:
        raise HTTPException(status_code=404)  # Always 404, never 403 — prevents enumeration
    return invoice

Option 2: Non-sequential, non-guessable identifiers

Replace auto-increment integer IDs with UUIDs or ULIDs. This doesn’t fix authorization logic, but it eliminates trivial enumeration:

import ulid

class Invoice(Base):
    id: str = Column(String, primary_key=True, default=lambda: str(ulid.new()))

Option 3: Relationship-scoped queries

In multi-tenant systems, scope all queries to the tenant context extracted from the JWT:

def get_invoices(current_user: User, db: Session):
    return (
        db.query(Invoice)
        .join(Organization)
        .filter(
            Organization.id == current_user.organization_id,
            Invoice.organization_id == current_user.organization_id,
        )
        .all()
    )

Understanding BFLA (Broken Function Level Authorization)

BFLA occurs when an API exposes administrative or privileged functions to users who should not have access — typically because authorization checks on function-level (not just data-level) are missing or incorrect.

Attack scenario:

# Regular user discovers admin endpoints via API spec exposure
GET /api/v1/admin/users          → 403 Forbidden (good)
DELETE /api/v1/users/54321       → 200 OK (should be 403 for non-admins!)
POST /api/v1/users/54321/promote → 200 OK (privilege escalation!)

Common root causes:

  1. Frontend hides admin buttons, but backend doesn’t enforce role checks
  2. API versioning gaps — /api/v2/ has checks, /api/v1/ endpoints were forgotten
  3. HTTP method confusion — GET /invoice/123 is authorized, DELETE /invoice/123 is not checked separately

Implementing Function-Level Authorization

Role-Based Access Control (RBAC) decorator pattern:

from functools import wraps
from enum import Enum

class Role(Enum):
    USER = "user"
    MANAGER = "manager"
    ADMIN = "admin"

ROLE_HIERARCHY = {Role.USER: 0, Role.MANAGER: 1, Role.ADMIN: 2}

def require_role(minimum_role: Role):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, current_user: User, **kwargs):
            if ROLE_HIERARCHY[current_user.role] < ROLE_HIERARCHY[minimum_role]:
                raise HTTPException(
                    status_code=403,
                    detail="Insufficient privileges"
                )
            return await func(*args, current_user=current_user, **kwargs)
        return wrapper
    return decorator

# Usage
@router.delete("/users/{user_id}")
@require_role(Role.ADMIN)
async def delete_user(user_id: str, current_user: User = Depends(get_current_user)):
    ...

@router.post("/users/{user_id}/promote")
@require_role(Role.ADMIN)
async def promote_user(user_id: str, current_user: User = Depends(get_current_user)):
    ...

Attribute-Based Access Control (ABAC) for fine-grained policies:

For complex enterprise systems, RBAC alone is insufficient. Use OPA as a centralized policy engine:

# opa/policies/api_authz.rego
package api.authz

default allow = false

# Users can only read their own resources
allow {
    input.method == "GET"
    input.path == ["api", "v1", "invoices", invoice_id]
    input.user.id == data.invoices[invoice_id].owner_id
}

# Managers can read any invoice within their organization
allow {
    input.method == "GET"
    input.path == ["api", "v1", "invoices", _]
    input.user.role == "manager"
    input.user.org_id == data.invoices[invoice_id].org_id
}

# Only admins can delete
allow {
    input.method == "DELETE"
    input.user.role == "admin"
}

API Gateway-Level Enforcement

Authorization logic must not live only in individual microservices — that creates drift and gaps. Enforce coarse-grained policies at the API gateway layer (Kong, AWS API Gateway, Apigee):

# Kong declarative config — rate limit and JWT validation
plugins:
  - name: jwt
    config:
      key_claim_name: kid
      claims_to_verify:
        - exp
        - iss
  - name: rate-limiting
    config:
      minute: 60
      hour: 1000
      policy: redis
  - name: opa
    config:
      server: http://opa-service:8181
      policy: api/authz/allow

Detection & Anomaly Monitoring

Authorization vulnerabilities that reach production require behavioral detection:

# Alert on BOLA enumeration patterns
rule = {
    "name": "API Object Enumeration Detected",
    "condition": (
        "same_user_id AND sequential_resource_ids AND "
        "request_count_1min > 50 AND "
        "response_mix_200_404"
    ),
    "action": ["block_user", "alert_security_team", "create_incident"],
}

Key signals to monitor:

  • Same user accessing sequential or high-volume distinct object IDs in short windows
  • 403/404 response volume spikes from a single source
  • Unexpected HTTP method usage on resource endpoints (DELETE, PATCH from user-tier tokens)

API Security Checklist

  • Object-level authorization enforced at query layer — never trust client-supplied IDs alone
  • Non-sequential identifiers (UUID/ULID) on all externally-exposed resources
  • Function-level authorization enforced server-side via RBAC/ABAC on every endpoint
  • HTTP method authorization verified — GET auth ≠ DELETE auth
  • API spec not publicly exposed (redact or gate OpenAPI/Swagger in production)
  • All API versions (v1, v2) have equivalent authorization coverage
  • API gateway enforcing JWT validation, rate limiting, and OPA policy evaluation
  • Behavioral anomaly detection alerting on enumeration patterns
  • Regular BOLA/BFLA-targeted penetration tests (quarterly minimum)

GurkhaX conducts API security audits, penetration testing, and authorization architecture reviews for microservices platforms. Request an API security assessment from our offensive security team.