Skip to content

Module 06: Broken Access Control

← Module 05: XSS and Client-Side | Topic Home | Next: Reconnaissance and OSINT →


Status Difficulty Time

Broken Access Control is OWASP's #1 vulnerability — because adding features is easy and authorization checks are consistently forgotten until it's too late.

[!NOTE] Stub Module — Full theory content will be expanded in a future update.


Table of Contents

  1. Overview
  2. Prerequisites
  3. Objectives
  4. Theory
  5. Key Concepts
  6. Examples
  7. Common Pitfalls
  8. Cross-Links
  9. Summary

Overview

Broken Access Control (OWASP A01:2021) has risen to the #1 position because authorization is consistently underdone. Authentication (verifying who you are) is usually implemented; authorization (verifying what you are allowed to do) is often missing or incomplete. The pattern appears everywhere: changing an ID parameter accesses another user's data, removing authentication tokens still works, changing a role parameter grants admin access.

This module covers the major broken access control patterns: Insecure Direct Object References (IDOR), privilege escalation (horizontal and vertical), mass assignment, JWT vulnerabilities, and missing function-level access control. The defensive framework is Role-Based Access Control (RBAC) and the principle of least privilege.


Prerequisites

  • [[pentesting-security/modules/03_web-application-security]] — OWASP Top 10, Burp Suite basics
  • Understanding of HTTP requests (parameters, headers, cookies)

Objectives

By the end of this module, you will be able to:

  1. Identify IDOR vulnerabilities by testing object references in authorized applications
  2. Explain horizontal vs. vertical privilege escalation with examples
  3. Demonstrate mass assignment vulnerabilities in API contexts
  4. Identify common JWT vulnerabilities (algorithm confusion, none algorithm, weak secrets)
  5. Design a RBAC system that prevents the access control failures covered in this module
  6. Apply the "deny by default" principle to authorization architecture

Theory

Insecure Direct Object References (IDOR)

IDOR occurs when an application uses user-controllable input to access objects without authorization verification. The object "reference" (ID, filename, account number) is directly exposed to the user, and the application trusts it without checking permissions.

# VULNERABLE: IDOR in API endpoint
from flask import Flask, request, jsonify

@app.route('/api/invoice/<int:invoice_id>')
def get_invoice(invoice_id):
    # MISSING: check that the current user owns this invoice
    invoice = db.get_invoice(invoice_id)
    return jsonify(invoice)
# Attacker logs in as user 1, requests /api/invoice/2 → sees user 2's invoice

# SAFE: enforce ownership check
from flask_login import current_user

@app.route('/api/invoice/<int:invoice_id>')
def get_invoice_safe(invoice_id):
    invoice = db.get_invoice(invoice_id)
    # Verify the authenticated user owns this resource
    if invoice is None or invoice.user_id != current_user.id:
        return jsonify({"error": "Not found"}), 404
    return jsonify(invoice)

Testing for IDOR: 1. Find any reference to an object ID in the URL, body, or headers 2. Note the ID format (integer, UUID, hash) 3. Authenticate as a second test user 4. Try to access the first user's objects using the second user's session 5. If successful: IDOR confirmed

Horizontal vs. Vertical Privilege Escalation

Horizontal escalation: User A accesses resources belonging to User B (same privilege level). IDOR is the canonical form. The attacker stays at the same role level but crosses user boundaries.

Vertical escalation: A lower-privileged user gains access to higher-privileged functions. Examples: a regular user accessing admin dashboard, a read-only user performing write operations.

# VULNERABLE: Vertical privilege escalation via hidden parameter
@app.route('/update_user', methods=['POST'])
def update_user():
    user_id = request.form['user_id']
    new_role = request.form.get('role', 'user')  # role comes from client
    # VULNERABLE: client controls the role assignment
    db.update_user_role(user_id, new_role)

# Attack: POST /update_user with role=admin
# The attacker promotes themselves to admin

# SAFE: role changes are admin-only operations
from functools import wraps

def admin_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if not current_user.is_admin:
            return jsonify({"error": "Forbidden"}), 403
        return f(*args, **kwargs)
    return decorated

@app.route('/update_user', methods=['POST'])
@admin_required  # Only admins can change roles
def update_user_safe():
    user_id = request.form['user_id']
    new_role = request.form['role']
    # Server-side validation: only allow valid role values
    if new_role not in ['user', 'moderator', 'admin']:
        return jsonify({"error": "Invalid role"}), 400
    db.update_user_role(user_id, new_role)

Mass Assignment

Mass assignment occurs when a server-side object is populated directly from user-supplied data without filtering which fields can be set. Common in frameworks with object-relational mapping.

# VULNERABLE: Mass assignment in a user profile update
@app.route('/api/profile', methods=['PUT'])
def update_profile():
    user_data = request.json  # Entire request body
    # VULNERABLE: updates ALL fields including is_admin, balance, etc.
    current_user.update(**user_data)
    return jsonify({"success": True})

# Attack: PUT /api/profile with {"name": "Hacker", "is_admin": true}

# SAFE: allowlist the fields that users can modify
ALLOWED_UPDATE_FIELDS = {'name', 'email', 'bio', 'avatar_url'}

@app.route('/api/profile_safe', methods=['PUT'])
def update_profile_safe():
    user_data = request.json
    # Only update fields in the allowlist
    safe_data = {k: v for k, v in user_data.items() if k in ALLOWED_UPDATE_FIELDS}
    current_user.update(**safe_data)
    return jsonify({"success": True})

JWT Vulnerabilities

JSON Web Tokens (JWTs) are widely used for stateless authentication. Common vulnerabilities:

import jwt  # PyJWT library

# VULNERABLE 1: Algorithm confusion — "none" algorithm
# Some libraries accept "alg": "none" which means no signature verification
# Attacker modifies payload and sets alg to "none"
# Fix: explicitly specify allowed algorithms in verification

# WRONG — allows any algorithm including "none"
decoded = jwt.decode(token, options={"verify_signature": False})

# RIGHT — specify exactly which algorithms are allowed
decoded = jwt.decode(token, secret_key, algorithms=["HS256"])

# VULNERABLE 2: Weak secret
# If the JWT is signed with a weak or default secret (e.g., "secret", "password"),
# it can be cracked offline using tools like hashcat or john
# Attack: hashcat -a 0 -m 16500 <jwt> /usr/share/wordlists/rockyou.txt
# Fix: use cryptographically random secrets of at least 256 bits

import secrets
# Generate a strong JWT secret
strong_secret = secrets.token_hex(32)  # 256-bit random secret

# VULNERABLE 3: Algorithm substitution (RS256 → HS256)
# If server uses RSA (RS256), attacker can sign with the PUBLIC KEY as HMAC secret
# by changing the algorithm to HS256 (symmetric HMAC)
# Fix: server must verify the algorithm matches what it expects

RBAC — Role-Based Access Control

The defensive architecture for preventing broken access control:

# Simple RBAC implementation
from enum import Enum

class Role(Enum):
    VIEWER = 1
    EDITOR = 2
    ADMIN = 3

class Permission(Enum):
    READ = 'read'
    WRITE = 'write'
    DELETE = 'delete'
    MANAGE_USERS = 'manage_users'

ROLE_PERMISSIONS = {
    Role.VIEWER: {Permission.READ},
    Role.EDITOR: {Permission.READ, Permission.WRITE},
    Role.ADMIN: {Permission.READ, Permission.WRITE, Permission.DELETE, Permission.MANAGE_USERS},
}

def has_permission(user, permission: Permission) -> bool:
    """Check if user has the required permission."""
    user_role = Role(user.role)
    return permission in ROLE_PERMISSIONS.get(user_role, set())

# Usage in a view
def delete_post(post_id):
    if not has_permission(current_user, Permission.DELETE):
        return {"error": "Forbidden"}, 403
    # Proceed with deletion only if authorized
    db.delete_post(post_id)

Key Concepts

IDOR (Insecure Direct Object Reference) — A vulnerability where user-controlled input directly references objects (database rows, files, accounts) without authorization verification. The fix is server-side ownership checks on every access.

Least Privilege — Users and systems should have only the minimum permissions needed to perform their intended functions. An API account that only needs to read data should have read-only database permissions.

Deny by Default — All access is denied unless explicitly permitted. The inverse (allow by default, deny exceptions) consistently leads to security gaps when new features are added.

RBAC (Role-Based Access Control) — An authorization model that assigns permissions to roles, and assigns roles to users. Users acquire permissions through their roles, not directly. Simplifies authorization management at scale.


Examples

Example 2: Testing for Vertical Privilege Escalation

# Test script for vertical privilege escalation (authorized lab only)
import requests

# Two sessions: regular user and admin user
user_session = requests.Session()
admin_session = requests.Session()

BASE_URL = "http://localhost"  # Local DVWA or authorized target

# Log in as regular user
user_session.post(f"{BASE_URL}/login", data={"username": "user", "password": "user123"})

# Log in as admin for comparison
admin_session.post(f"{BASE_URL}/login", data={"username": "admin", "password": "admin123"})

# Get list of admin-only endpoints to test
admin_endpoints = [
    "/admin/users",
    "/admin/settings",
    "/api/admin/stats",
    "/management/",
]

print("Testing vertical privilege escalation:")
for endpoint in admin_endpoints:
    admin_r = admin_session.get(f"{BASE_URL}{endpoint}")
    user_r = user_session.get(f"{BASE_URL}{endpoint}")

    if admin_r.status_code == 200 and user_r.status_code == 200:
        print(f"[!] POSSIBLE ESCALATION: {endpoint} — admin=200, user=200")
    elif user_r.status_code == 403:
        print(f"[OK] {endpoint} — correctly returns 403 for regular user")
    else:
        print(f"[?] {endpoint} — admin={admin_r.status_code}, user={user_r.status_code}")

Common Pitfalls

Pitfall 1: Client-side access control only. Hiding admin buttons in the UI does not prevent API calls. Every function must be protected server-side regardless of what the UI shows.

Pitfall 2: Trusting JWT claims without verification. Decoding a JWT (base64 decode) is not the same as verifying it. Always verify the signature and check the expected claims (audience, issuer, expiry).

Pitfall 3: Forgetting indirect references. Changing ?user_id=123 to ?user_id=456 is obvious IDOR. But access control also applies to indirect references: file downloads (/downloads/report.pdf), image URLs (/uploads/avatar_456.jpg), API responses that include other users' data in nested objects.


  • [[pentesting-security/modules/03_web-application-security]] — OWASP A01: Broken Access Control overview
  • [[pentesting-security/modules/08_exploitation-basics]] — Burp Suite for IDOR testing
  • [[pentesting-security/modules/11_bug-bounty-program]] — IDOR findings are highly valued in bug bounty programs

Summary

  • Broken Access Control is OWASP #1 because authorization is consistently missed or incomplete
  • IDOR: user-controlled object references without ownership checks — fix with server-side authorization
  • Horizontal escalation: accessing peer users' data; vertical escalation: accessing higher-privilege functions
  • Mass assignment: API objects populated from user input without field filtering — use allowlists
  • JWT vulnerabilities include "none" algorithm, weak secrets, and algorithm substitution
  • RBAC and deny-by-default are the correct architectural defenses
  • Every access control check must be server-side — client-side hiding is not a security control