Module 06: Broken Access Control¶
← Module 05: XSS and Client-Side | Topic Home | Next: Reconnaissance and OSINT →
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¶
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:
- Identify IDOR vulnerabilities by testing object references in authorized applications
- Explain horizontal vs. vertical privilege escalation with examples
- Demonstrate mass assignment vulnerabilities in API contexts
- Identify common JWT vulnerabilities (algorithm confusion, none algorithm, weak secrets)
- Design a RBAC system that prevents the access control failures covered in this module
- 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.
Cross-Links¶
- [[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