The XSS Rat OWASP Lab by The XSS Rat
A01:2025 Critical

Broken Access Control

Theory

Broken Access Control held #1 in the 2025 edition, as it did in 2021 — and in 2025 it is the widest category on the list, with 40 mapped CWEs including SSRF (CWE-918) and CSRF (CWE-352). Access control enforces that users can only act within their intended permissions. When it fails, attackers can read other users' data, call admin-only functions, or escalate their own privileges.

Types of Broken Access Control

  • IDOR (Insecure Direct Object Reference) — changing a resource ID in a URL to access another user's record: GET /orders/1337 where 1337 is not your order
  • Missing Function-Level Access Control — admin panels or API routes are reachable with zero auth checks; the route simply exists and returns data
  • Path Traversal — escaping the intended directory: GET /download?file=../../etc/passwd
  • Horizontal Privilege Escalation — user A can read/modify user B's data (same role, different account)
  • Vertical Privilege Escalation — a regular user reaches admin-only actions (different role)
  • CORS Misconfiguration — Access-Control-Allow-Origin: * on sensitive endpoints lets any origin read the response

Real-World Breaches

  • First American Financial (2019) — 885 million mortgage documents leaked; anyone could view any document by incrementing a number in the URL. No authentication required.
  • Parler (2021) — All 70 million posts were scraped in bulk before shutdown because post IDs were sequential and no access check existed on the API
  • Facebook (2018) — An IDOR in the video upload flow allowed any logged-in user to access any other user's private videos
  • Venmo (2019) — The public transaction API required no authentication, exposing 207 million transactions including payees and amounts

Vulnerable Code Pattern

# VULNERABLE — no ownership check; any authenticated user can read any order
@app.get("/orders/{order_id}")
def get_order(order_id: int, current_user = Depends(get_current_user)):
    order = db.get(Order, order_id)   # fetches ANY row — never checks ownership!
    if not order:
        raise HTTPException(404)
    return order   # leaks data belonging to other users

How the Attack Works — Step by Step

# Attacker: alice (user_id=2). Target: admin order (user_id=1)

# Step 1 — observe your own order to learn the ID format
GET /orders/5  →  {"id": 5, "user_id": 2, "product": "Widget"}

# Step 2 — enumerate other IDs; the server never checks ownership
GET /orders/1  →  {"id": 1, "user_id": 1, "secret_note": "flag{...}"}
GET /orders/2  →  {"id": 2, "user_id": 1, "product": "Confidential"}

# Automate with a simple loop:
for i in range(1, 1000):
    r = requests.get(f"https://target.com/orders/{i}", headers=auth)
    if r.status_code == 200:
        print(i, r.json())

Fixed Code

# FIXED — ownership enforced inside the database query
@app.get("/orders/{order_id}")
def get_order(order_id: int, current_user = Depends(get_current_user)):
    order = db.exec(
        select(Order)
        .where(Order.id == order_id)
        .where(Order.user_id == current_user.id)   # ownership in the WHERE clause
    ).first()
    if not order:
        raise HTTPException(404)   # same 404 for not-found AND not-owned — no info leak
    return order

# For admin routes, enforce via a reusable dependency:
def require_admin(user = Depends(get_current_user)):
    if user.role != "admin":
        raise HTTPException(403)
    return user

@app.get("/admin/users")
def list_all_users(_admin = Depends(require_admin)):   # fails fast if not admin
    return db.exec(select(User)).all()

New in 2025: SSRF folded into this category

The 2021 edition listed Server-Side Request Forgery as its own category (A10:2021). In 2025 it is one of the 40 CWEs mapped to Broken Access Control — CWE-918 — because an SSRF is the server being tricked into reaching a resource the caller was never authorised to reach.

  • Internal-only services — ?url=http://127.0.0.1:8000/admin reaches endpoints that are firewalled from the internet but trusted from localhost
  • Cloud metadata — http://169.254.169.254/latest/meta-data/ hands out instance credentials on unpatched IMDSv1 hosts
  • Alternate schemes — file://, gopher:// and dict:// turn a URL fetcher into a file reader or a protocol smuggler
# VULNERABLE — fetches whatever the user supplies
@app.get("/fetch")
async def fetch(url: str):
    async with httpx.AsyncClient() as client:
        return (await client.get(url)).text      # no scheme allow-list, no IP filter

# FIXED — allow-list the scheme AND resolve+check the destination IP
import ipaddress, socket
ALLOWED_SCHEMES = {"http", "https"}

async def fetch_safe(url: str):
    parts = urlparse(url)
    if parts.scheme not in ALLOWED_SCHEMES:
        raise ValueError("scheme not allowed")
    ip = ipaddress.ip_address(socket.gethostbyname(parts.hostname))
    if ip.is_private or ip.is_loopback or ip.is_link_local:
        raise ValueError("destination not allowed")   # blocks 127/8, 10/8, 169.254/16
    async with httpx.AsyncClient() as client:
        return (await client.get(url)).text

How to Fix — Checklist

  • Deny by default — every route requires authentication unless explicitly marked public
  • Enforce ownership in the query — always filter by user_id = current_user.id; never trust a client-supplied ID alone
  • Use indirect references — UUIDs instead of sequential integers make enumeration harder (but are not a substitute for proper auth checks)
  • Two-account test — during every code review: can user A access user B's resources? Automate this check in CI
  • Log & alert on failures — repeated 403/404 on sequential IDs is a sign of IDOR scanning
Challenge 1

IDOR — Read another user's private profile

The profile endpoint is publicly accessible — no login required. It returns any user's private data based on the user_id URL parameter with zero authorisation checks. The form defaults to user_id=2 (alice's profile). Change it to 1 (admin) and click Submit to capture the flag.

Steps
Set user_id to 1 in the form below and click Submit.
Challenge 2

Missing Function-Level Access Control — Unauthenticated Admin Panel

The admin panel has no authentication check. Any user (or no user at all) can access it directly.

Steps
Visit /web/a01/admin directly.
Challenge 3

SSRF — Reach an internal-only endpoint (CWE-918)

The URL fetcher makes a server-side request to any URL you give it. An internal endpoint refuses connections from the internet but trusts localhost — make the server fetch it for you.

Steps
Try URL: http://127.0.0.1:8000/web/internal/ssrf-flag
Challenge 4

SSRF — Read local files via the file:// scheme

The same fetcher never allow-lists the URL scheme, so it will happily read from the container filesystem instead of the network.

Steps
Try URL: file:///app/ssrf_flag.txt