Skip to content

CWE-284: Improper Access Control

Overview

Improper access control is MITRE's broadest, top-level term for any failure that lets a user reach data or functionality they should not be able to reach. It is a Pillar weakness - the umbrella over the entire access-control family. Its two largest branches are CWE-287 (Improper Authentication, failing to correctly verify who is making the request) and CWE-285 (Improper Authorization, failing to correctly enforce what an already-identified user is allowed to do), and there are several more alongside them, covering privileges, ownership and request origin. Real findings are almost always one of those descendants rather than this entry, and it is the descendant pages that carry the concrete guidance.

Relationship to Other CWEs

CWE-284 is a MITRE Pillar, and MITRE's mapping guidance is Discouraged - real-world findings should map to a more specific descendant instead. MITRE's own note nominates seven better targets - CWE-862, CWE-863, CWE-732, CWE-306, CWE-1390, CWE-286 and CWE-923 - and pages here cover the first four, along with several of their relatives. How this page and those descendants relate:

Credential weaknesses are a separate branch that MITRE files elsewhere in the research view: CWE-522 (Insufficiently Protected Credentials) sits under CWE-668 and CWE-1390, and CWE-798 (Use of Hard-coded Credentials) under CWE-1391. Both appear beneath CWE-287 in MITRE's simplified mapping view (view-1003), which MITRE publishes for categorising published vulnerability data such as NVD's, so a finding whose CWE came from a CVE record may route them here.

Use this page only for general access-control hygiene when a finding is reported generically, without a more specific CWE.

OWASP Classification

A01:2025 - Broken Access Control

Risk

High: 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, or health information beyond what a user is authorized to see
  • Business logic bypass: Circumventing payment, approval workflows, or rate limits
  • Compliance violations: Unauthorized data access can violate GDPR, HIPAA, SOC 2

Remediation Steps

Core Principle: Access control must be explicit and deny-by-default across every entry point and resource; never let untrusted input or client-side flow determine authentication or authorization.

Trace the Data Path

  • Source: The identity or permission claim behind the request - a session, token, cookie, or role/permission asserted by the client.
  • Sink: The protected resource or operation - an endpoint, function, file, or record being read, modified, or invoked.
  • Missing Controls: No server-side check that the request is authenticated (CWE-287) and, separately, that the authenticated identity is authorized for this specific resource and action (CWE-285).

Enforce Authentication and Authorization on Every Request (Primary Defense)

  1. Authenticate the user: verify identity via a server-validated session/token; missing or invalid credentials return 401 Unauthorized.
  2. Load the user's roles/permissions from a trusted, server-side source - never from client-supplied data.
  3. Check permission for the specific action: if not userHasPermission(action): return 403 Forbidden.
  4. Enforce object-level authorization, not just function-level. Prefer making ownership part of the question - WHERE id = ? AND owner_id = ?, a repository method requiring both, a queryset already scoped to the caller - rather than a check applied to the row after it is fetched, so a route that forgets the check is still confined to rows the caller could have seen. Give the "not yours" and the "does not exist" outcomes the same response.
  5. Only then perform the action.

Default Deny, Not Default Allow

Explicitly require authentication and authorization for every route; treat "public" as the exception that must be marked, not the default:

VULNERABLE - allowlist of protected paths (easy to miss a new one):
if path == "/admin" or path == "/settings":
    requireAuth()

SECURE - default deny, explicit public exceptions:
PUBLIC_PATHS = ["/login", "/register", "/public/*"]
for every request:
    if path not in PUBLIC_PATHS:
        requireAuthentication()
        requireAuthorization()

Apply Additional Protections (Defense in Depth)

  • Centralize the checks: Use middleware, decorators, or a shared authorization service - checks scattered across handlers are how gaps happen.
  • Use indirect object references: UUIDs or session-scoped opaque IDs reduce enumeration, but never replace the object-level authorization check itself.
  • Apply least privilege: Grant only the permissions a role needs; review and revoke unused grants on a cadence appropriate to risk.
  • Monitor and audit: Log authorization decisions and permission changes (safe metadata only), and alert on repeated 403s or privilege-escalation attempts.

Test the Fix

  • Horizontal: as User B, attempt to read, modify, and delete User A's resources by ID - every attempt must be denied.
  • Vertical: as a regular user, attempt admin endpoints directly (URL, API call) and by tampering with a role claim in a token or cookie.
  • Confirm unauthenticated requests get 401. For an authenticated-but-unauthorized request that names no particular record - an admin route, a privileged action - 403 is the right answer. Where the request names an object, the refusal for someone else's object and the refusal for an ID that exists in no row must be identical in status and body, or the pair enumerates the table one request at a time; see CWE-639.
  • Re-scan with the security scanner to confirm the finding is resolved.

Common Vulnerable Patterns

// VULNERABLE - no authentication or authorization check
function adminDeleteUser(userId):
    database.deleteUser(userId)   // reachable by anyone, any role

Why this is vulnerable: the function is named for an administrative action and there is nothing between the caller and the deletion. No identity is established, no role is consulted, and the resource identifier arrives from the request - so the control is not weak or bypassable, it is absent. Every statement present is correct; what is missing was never written, so there is no wrong line for a reviewer or a scanner to find.

Because this is the pillar of the family, a finding filed here is a starting point rather than a diagnosis. The useful next question is which of the three things is missing, because they have different fixes: nobody proved who the caller is (CWE-306 where no check exists on the path, CWE-287 where one exists and is bypassable), nobody checked what that caller may do (CWE-862, or CWE-863 where the check runs and decides wrongly), or the check ran and did not cover this particular object (CWE-639). Answering that determines whether the fix belongs in middleware, in the handler, or in the query - and a fix applied at the wrong one of those layers leaves the endpoint reachable by a route that skips it.

Secure Patterns

// SECURE - authenticate, then authorize the specific action
function adminDeleteUser(userId, request):
    currentUser = authenticate(request)          // 401 if missing/invalid
    if not currentUser.hasRole("ADMIN"):
        return 403
    database.deleteUser(userId)

Why this works: The operation is unreachable until the caller's identity is verified server-side, and unreachable a second time unless that identity carries the specific permission the action requires. Neither check can be skipped by a client that edits a request, disables JavaScript, or guesses a URL, because both are enforced on the server rather than inferred from anything the client sent.

Common Pitfalls

  • Treating "authenticated" as "authorized": Assuming that because a request passed login/session checks, the user is automatically allowed to perform the requested action - authentication only proves identity; a separate, explicit permission check is still required for every action and object.
  • Fixing the flagged endpoint without auditing its siblings: Adding a missing check to the reported GET route while the PUT/DELETE/export route for the same resource is left unchecked - access control gaps rarely exist in isolation.
  • Mapping every access-control finding to this page instead of the specific weakness: CWE-284 is too broad to give the concrete fix; identify whether the gap is really CWE-285 (authorization), CWE-287 (authentication), or one of their more specific descendants, and use that page's guidance.
  • Relying on an unlisted or hard-to-guess URL as the control: An admin path that isn't linked from the UI is still reachable by anyone who requests it directly; obscurity is not a substitute for a server-enforced identity and permission check.

Additional Resources