Incorrect Authorization

Description

Incorrect Authorization occurs when a product performs an authorization check when an actor attempts to access a resource or perform an action, but the check is implemented incorrectly. Unlike CWE-862 (Missing Authorization) where no check exists, CWE-863 involves a check that exists but doesn't work properly. Common issues include: checking the wrong user or role, using incorrect comparison logic, failing to account for edge cases, race conditions in authorization checks, caching issues that don't reflect revoked permissions, and authorization bypass through parameter manipulation.

Risk

Incorrect authorization has been consistently ranked in the CWE Top 25. CVE-2025-64707 in Frappe LMS demonstrates a caching flaw where role revocations aren't immediately enforced, allowing users to retain permissions temporarily after admin removal. Recent vulnerabilities affect GitLab CE/EE, OpenStack Keystone (Critical), JumpServer, Authlib, Adobe Commerce/Magento, and Kubernetes apiserver. These vulnerabilities are particularly dangerous because they may pass security audits that only check for the presence of authorization code, not its correctness.

Solution

Implement authorization using well-tested frameworks rather than custom code. Use centralized authorization services. Invalidate caches immediately when permissions change. Implement defense in depth with multiple authorization checks. Test authorization logic thoroughly including edge cases and race conditions. Use positive authorization (explicitly allow) rather than negative (deny specific cases). Log all authorization decisions for audit. Implement fail-secure defaults—deny access if authorization check fails.

Common Consequences

ImpactDetails
Access ControlScope: Privilege Escalation

Users gain access to resources or actions beyond their authorization level.
IntegrityScope: Unauthorized Modification

Attackers can modify data they shouldn't have access to due to flawed checks.
ConfidentialityScope: Data Exposure

Incorrect authorization exposes sensitive data to unauthorized users.

Example Code + Solution Code

Vulnerable Code

# VULNERABLE: Wrong comparison operator
def check_admin(user):
    # Should be == but using = (assignment in some languages)
    # In Python this is syntax error, but logic errors are common
    return user.role = 'admin'  # SyntaxError, but shows concept

# VULNERABLE: Case-sensitive comparison fails
def is_authorized(user_role, required_role):
    # "Admin" != "admin" - user bypasses by using different case
    return user_role == required_role

# VULNERABLE: Checking wrong user object
@app.route('/api/admin/users/<user_id>')
@login_required
def admin_get_user(user_id):
    target_user = User.query.get(user_id)
    # BUG: Checking target_user's role instead of current_user!
    if target_user.is_admin:  # Should be current_user.is_admin
        return jsonify(target_user.to_dict())
    return 'Forbidden', 403

# VULNERABLE: Race condition in permission check
class PermissionService:
    def __init__(self):
        self.permission_cache = {}

    def check_permission(self, user_id, resource):
        # Check cache first
        cache_key = f"{user_id}:{resource}"
        if cache_key in self.permission_cache:
            return self.permission_cache[cache_key]

        # DB check
        has_permission = self.db_check(user_id, resource)
        self.permission_cache[cache_key] = has_permission
        return has_permission

    # When permission is revoked, cache isn't invalidated!
    def revoke_permission(self, user_id, resource):
        self.db_revoke(user_id, resource)
        # Missing: del self.permission_cache[f"{user_id}:{resource}"]
// VULNERABLE: Integer overflow in age check
public boolean canAccessAdultContent(User user) {
    int age = user.getAge();
    // If age is negative (corrupted data), this passes!
    return age >= 18;
}

// VULNERABLE: Null check bypass
public boolean hasPermission(User user, String permission) {
    List<String> permissions = user.getPermissions();
    // If permissions is null, this throws NPE
    // Attacker might trigger null permissions to cause error
    return permissions.contains(permission);
}

// VULNERABLE: Time-of-check to time-of-use (TOCTOU)
public void deleteDocument(Long docId, User user) {
    Document doc = documentRepository.findById(docId);

    // Check authorization
    if (!doc.getOwnerId().equals(user.getId())) {
        throw new UnauthorizedException();
    }

    // Gap between check and use - doc could be reassigned!
    performExpensiveOperation();

    // By now, doc owner might have changed
    documentRepository.delete(doc);
}
// VULNERABLE: Type coercion in comparison
function checkRole(user, requiredRole) {
    // "1" == 1 is true in JavaScript - type coercion bypass
    return user.roleId == requiredRole;
}

// VULNERABLE: Prototype pollution affecting authorization
function isAdmin(user) {
    // If Object.prototype.isAdmin was polluted, all objects are admins!
    return user.isAdmin === true;
}

// VULNERABLE: Array includes with wrong type
function hasPermission(user, permission) {
    // permissions might be strings, permission might be object
    // or vice versa - comparison fails silently
    return user.permissions.includes(permission);
}

// VULNERABLE: Async authorization race condition
async function accessResource(userId, resourceId) {
    const hasAccess = await checkAccess(userId, resourceId);

    if (hasAccess) {
        // Access could be revoked between check and use!
        await new Promise(r => setTimeout(r, 1000)); // Simulates delay
        return await getResource(resourceId);
    }
}

Fixed Code

# SAFE: Proper comparison with validation
def check_admin_safe(user):
    if user is None:
        return False
    return user.role == 'admin'

# SAFE: Case-insensitive comparison
def is_authorized_safe(user_role, required_role):
    if not user_role or not required_role:
        return False
    return user_role.lower() == required_role.lower()

# SAFE: Check correct user and handle edge cases
@app.route('/api/admin/users/<user_id>')
@login_required
def admin_get_user_safe(user_id):
    # Check CURRENT user's admin status, not target user
    if not current_user.is_admin:
        audit_log.warning(f"Non-admin {current_user.id} attempted admin access")
        return 'Forbidden', 403

    target_user = User.query.get_or_404(user_id)
    audit_log.info(f"Admin {current_user.id} accessed user {user_id}")
    return jsonify(target_user.to_dict())

# SAFE: Cache invalidation on permission changes
class SecurePermissionService:
    def __init__(self):
        self._cache = {}
        self._lock = threading.Lock()

    def check_permission(self, user_id, resource):
        cache_key = f"{user_id}:{resource}"

        with self._lock:
            if cache_key in self._cache:
                cached = self._cache[cache_key]
                if cached['expires'] > time.time():
                    return cached['value']

            has_permission = self._db_check(user_id, resource)

            self._cache[cache_key] = {
                'value': has_permission,
                'expires': time.time() + 60  # Short TTL
            }
            return has_permission

    def revoke_permission(self, user_id, resource):
        self._db_revoke(user_id, resource)

        # Invalidate cache immediately
        cache_key = f"{user_id}:{resource}"
        with self._lock:
            self._cache.pop(cache_key, None)

        # Also invalidate any related caches
        self._invalidate_user_cache(user_id)
// SAFE: Proper validation with defensive checks
public boolean canAccessAdultContent(User user) {
    if (user == null) {
        return false;
    }

    Integer age = user.getAge();
    if (age == null || age < 0 || age > 150) {
        logger.warn("Invalid age value: {} for user {}", age, user.getId());
        return false;  // Fail secure
    }

    return age >= 18;
}

// SAFE: Null-safe permission check
public boolean hasPermission(User user, String permission) {
    if (user == null || permission == null) {
        return false;
    }

    List<String> permissions = user.getPermissions();
    if (permissions == null || permissions.isEmpty()) {
        return false;
    }

    return permissions.contains(permission);
}

// SAFE: Atomic authorization check and action
@Transactional(isolation = Isolation.SERIALIZABLE)
public void deleteDocumentSafe(Long docId, User user) {
    // Lock the document row
    Document doc = documentRepository.findByIdWithLock(docId)
        .orElseThrow(() -> new NotFoundException("Document not found"));

    // Authorization check with locked row
    if (!doc.getOwnerId().equals(user.getId()) && !user.isAdmin()) {
        auditService.logUnauthorizedAccess(user.getId(), "document", docId);
        throw new UnauthorizedException("Not authorized to delete this document");
    }

    // Delete while still holding lock
    documentRepository.delete(doc);
    auditService.logDeletion(user.getId(), "document", docId);
}
// SAFE: Strict equality comparison
function checkRole(user, requiredRole) {
    if (!user || user.roleId === undefined) {
        return false;
    }
    // Use strict equality to prevent type coercion
    return user.roleId === requiredRole;
}

// SAFE: Prototype pollution resistant
function isAdmin(user) {
    if (!user || typeof user !== 'object') {
        return false;
    }
    // Check own property, not inherited
    return Object.prototype.hasOwnProperty.call(user, 'isAdmin') &&
           user.isAdmin === true;
}

// SAFE: Proper type checking in array search
function hasPermission(user, permission) {
    if (!user || !Array.isArray(user.permissions)) {
        return false;
    }
    if (typeof permission !== 'string') {
        return false;
    }
    return user.permissions.some(p =>
        typeof p === 'string' && p === permission
    );
}

// SAFE: Re-check authorization at action time
async function accessResourceSafe(userId, resourceId) {
    // Acquire lock and check authorization atomically
    const lock = await acquireLock(`resource:${resourceId}`);

    try {
        // Check authorization while holding lock
        const hasAccess = await checkAccess(userId, resourceId);

        if (!hasAccess) {
            throw new UnauthorizedError('Access denied');
        }

        // Access resource while still holding lock
        const resource = await getResource(resourceId);

        return resource;
    } finally {
        await releaseLock(lock);
    }
}

Exploited in the Wild

Frappe LMS Role Caching Flaw (Frappe, 2025)

CVE-2025-64707 in Frappe LMS 2.0.0-2.41.0 allows users to retain permissions after role revocation due to caching mechanisms that delay enforcement of permission changes, fixed by implementing immediate cache invalidation.

OpenStack Keystone Authorization Bypass (OpenStack, 2025)

Critical incorrect authorization vulnerability in OpenStack Keystone allows attackers to bypass access controls through flawed authorization logic.

Kubernetes API Server Privilege Escalation (Kubernetes, 2025)

Incorrect authorization in Kubernetes apiserver allows privilege escalation through flawed authorization checks.


Tools to test/exploit


CVE Examples


References

  1. MITRE. "CWE-863: Incorrect Authorization." https://cwe.mitre.org/data/definitions/863.html

  2. OWASP. "Access Control Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html