Skip to content

CWE-284: Improper Access Control

Overview

Improper access control occurs when an application fails to properly restrict what authenticated users are allowed to do. While authentication verifies "who you are," access control (authorization) verifies "what you're allowed to access." This is distinct from authentication (CWE-287) - you can have strong authentication but weak authorization, allowing authenticated users to access data they shouldn't see.

OWASP Classification

A01:2025 - Broken Access Control

Risk

Access control failures enable unauthorized access to sensitive functionality and data:

  • Horizontal privilege escalation: Users access other users' data (view other customers' orders, read others' emails)
  • Vertical privilege escalation: Regular users gain admin privileges (delete users, modify system settings)
  • Data breaches: Access to PII, financial records, health information beyond authorization
  • Business logic bypass: Circumvent payment, approval workflows, rate limits
  • Account takeover: Modify other users' profiles, passwords, email addresses
  • Compliance violations: Unauthorized data access violates GDPR, HIPAA, SOC 2
  • System compromise: Admin function access enables full system control

Access control is the #1 risk in OWASP Top 10 2025, reflecting its prevalence and critical impact.

Common Access Control Failures

Authorization failures manifest in multiple ways:

  • Missing authorization checks: Functions execute without verifying user permissions
  • Insecure direct object references (IDOR): Users access resources by manipulating IDs (changing /api/user/123 to /api/user/124)
  • Privilege escalation: Regular users access admin functions through direct URLs or API calls
  • Function-level access control: UI hides admin buttons, but APIs don't check permissions
  • Metadata manipulation: Tampering with JWTs, cookies, or hidden fields to elevate privileges
  • CORS misconfiguration: Allowing unauthorized cross-origin access to sensitive APIs

Remediation Steps

Core principle: Access control must be explicit and deny-by-default across all entry points and resources; never allow untrusted input or application flow to determine authorization.

Locate the access control vulnerability in your application

  • Review the flaw details to identify the specific file, line number, and code pattern
  • Identify the protected resource or function lacking authorization checks (admin panel, user data, API endpoint, sensitive operation)
  • Trace the data flow: how is the resource accessed (direct URL, API call, parameter manipulation)
  • Determine the risk: what can an attacker access or modify without proper authorization (other users' data, admin functions, system settings)

Enforce server-side authorization checks on every request (Primary Defense)

  • Check permissions on EVERY protected operation:
    1. Authenticate the user (verify identity): if (!session.isAuthenticated()) return 401 Unauthorized
    2. Load user's permissions/roles: userRoles = getCurrentUser().getRoles()
    3. Check permission for specific resource/action: if (!userRoles.contains(REQUIRED_PERMISSION)) return 403 Forbidden
    4. Verify object-level authorization (ownership): if (resource.ownerId != currentUser.id && !currentUser.isAdmin()) return 403 Forbidden
    5. Perform the authorized action: return performAction(resource)
  • Key principles: Check authorization on EVERY request (never assume), enforce server-side (never trust client-side), default deny (require explicit permission)

Implement role-based access control (RBAC) or attribute-based access control (ABAC)

  • Define clear roles: GUEST (browse public), USER (manage own data), MODERATOR (moderate content), ADMIN (full access)
  • Define permissions per role: USER_PERMISSIONS = ["read:own_profile", "update:own_profile"], ADMIN_PERMISSIONS = ["read:all_users", "delete:user"]
  • Check permission before action: if (!currentUser.hasPermission("delete:user")) throw ForbiddenException
  • Use framework support: Java @PreAuthorize("hasRole('ADMIN')"), .NET [Authorize(Roles = "Admin")], Django @permission_required('app.delete_user')

Apply additional access control protections

  • Use indirect object references: Use UUIDs instead of sequential IDs (replace /api/documents/1234 with /api/documents/a3d5f7e9-4b2c-4a1e-b8f6-3c9d5e7f8a1b)
  • Implement access control matrix: Map which roles can perform which actions on which resource types
  • Apply principle of least privilege: Grant minimum necessary permissions, review and revoke unused permissions
  • Centralize authorization logic: Use middleware, decorators, or authorization service - don't scatter checks throughout code

Monitor and audit access control

  • Log all access attempts and permission changes (successful and failed authorization attempts)
  • Alert on suspicious or unauthorized activity (privilege escalation attempts, unusual access patterns, repeated 403 errors)
  • Regularly review and update access controls (quarterly access reviews, role audits)
  • Monitor for IDOR attempts (sequential ID enumeration, accessing other users' resources)

Test the access control fix thoroughly

  • Test horizontal privilege escalation: User A cannot access User B's data
  • Test vertical privilege escalation: Regular user cannot access admin functions
  • Test with different roles (guest, user, moderator, admin)
  • Test direct URL access to protected resources
  • Test API endpoints with different user contexts
  • Re-scan with security scanner to confirm the issue is resolved

Use session-specific mappings

Server creates mapping: session.docId123 → database.id=1234 GET /api/documents/123 // 123 only valid in this user's session

Always verify ownership regardless

function getDocument(docId) {
    doc = database.query("SELECT * FROM docs WHERE id = ?", docId)
    if (doc.ownerId != currentUser.id && !currentUser.canViewAllDocs()) {
        throw ForbiddenException()
    }
    return doc
}

Deny by Default

Explicitly require permissions; never assume access:

Configuration approach:

BAD - Allowlist exceptions:
if (path == "/admin" || path == "/settings") checkAuth()
// Easy to miss new admin endpoints

GOOD - Default deny:
for every request:
    if not in PUBLIC_PATHS:
        requireAuthentication()
        requireAuthorization()

Framework examples:

- Spring Security: .anyRequest().authenticated()
- ASP.NET: [Authorize] on base controller
- Express.js: app.use(requireAuth) as default middleware

Public paths explicitly marked:
PUBLIC_PATHS = ["/login", "/register", "/public/*"]

1. Horizontal Privilege Escalation

Test as User A:

  1. Access own resource: GET /api/orders/100 (User A's order) → 200 OK
  2. Note resource ID pattern

Test as User B: 3. Try accessing User A's resource: GET /api/orders/100 → 403 Forbidden 4. Try modifying User A's resource: PUT /api/orders/100 → 403 Forbidden 5. Try deleting User A's resource: DELETE /api/orders/100 → 403 Forbidden

Expected: All cross-user access attempts denied

Vertical Privilege Escalation

Test as regular user:

  1. Access admin endpoint: GET /admin/users → 403 Forbidden
  2. Call admin API: POST /api/admin/delete-user → 403 Forbidden
  3. Modify JWT/cookie role: Change "role":"user" to "role":"admin" → Rejected or ignored
  4. Access hidden admin URLs directly → 403 Forbidden

Expected: No admin function accessible to regular users

IDOR Testing

Sequential ID enumeration:

  • GET /api/documents/1 → 200 OK (own document)
  • GET /api/documents/2 → 403 Forbidden (someone else's)
  • GET /api/documents/3 → 403 Forbidden
  • PUT /api/users/5/email → 403 Forbidden (can't modify other user)

Expected: Can only access owned resources

Function-Level Access Control

Test each protected function:

  • Admin panel: Try accessing /admin without admin role
  • Delete operations: Try DELETE as unauthorized user
  • Export functions: Try /api/export/all-users as regular user
  • Settings: Try POST /api/settings without admin permission

Expected: All require appropriate permissions

Automated Testing

# Test authorization matrix

test_cases = [
    {"role": "guest", "endpoint": "/api/profile", "expected": 401},
    {"role": "user", "endpoint": "/api/admin", "expected": 403},
    {"role": "user", "endpoint": "/api/profile", "expected": 200},
    {"role": "admin", "endpoint": "/api/admin", "expected": 200},
]

for test in test_cases:
    response = api_call(test["endpoint"], role=test["role"])
    assert response.status == test["expected"]

Dynamic Scan Guidance

For guidance on remediating this CWE when detected by dynamic (DAST) scanners:

Additional Resources