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/123to/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:
- Authenticate the user (verify identity):
if (!session.isAuthenticated()) return 401 Unauthorized - Load user's permissions/roles:
userRoles = getCurrentUser().getRoles() - Check permission for specific resource/action:
if (!userRoles.contains(REQUIRED_PERMISSION)) return 403 Forbidden - Verify object-level authorization (ownership):
if (resource.ownerId != currentUser.id && !currentUser.isAdmin()) return 403 Forbidden - Perform the authorized action:
return performAction(resource)
- Authenticate the user (verify identity):
- 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/1234with/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:
- Access own resource:
GET /api/orders/100(User A's order) → 200 OK - 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:
- Access admin endpoint:
GET /admin/users→ 403 Forbidden - Call admin API:
POST /api/admin/delete-user→ 403 Forbidden - Modify JWT/cookie role: Change "role":"user" to "role":"admin" → Rejected or ignored
- 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 ForbiddenPUT /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:
- Dynamic Scan Guidance - Analyzing DAST findings and mapping to source code