Missing Authorization

Description

Missing Authorization occurs when a product does not perform an authorization check when an actor attempts to access a resource or perform an action. While the product may perform authentication to verify the actor's identity, it fails to verify that the authenticated user is actually permitted to access the specific resource or perform the requested action. This is the root cause of Insecure Direct Object Reference (IDOR) vulnerabilities, where users can access other users' data by manipulating identifiers like user IDs, document IDs, or order numbers.

Risk

Missing authorization is consistently ranked in the CWE Top 25 Most Dangerous Software Weaknesses. CVE-2025-36367 in IBM i (versions 7.2-7.6) allows authenticated attackers to escalate privileges to root through missing authorization checks in SQL services. CVE-2025-11154 in IDonate WordPress plugin allows unauthenticated attackers to delete arbitrary user accounts. CVE-2025-12980 in PostX WordPress plugin exposes password hashes through an unauthenticated REST API endpoint (CVSS 7.5). Recent vulnerabilities affect SonicWall SMA1000, Apple iOS 26, and numerous WordPress plugins. IDOR vulnerabilities have led to massive data breaches exposing millions of user records.

Solution

Implement authorization checks for every resource access and action. Use role-based access control (RBAC) or attribute-based access control (ABAC). Verify resource ownership before allowing access. Implement authorization at the service/business logic layer, not just the UI. Use indirect references (mapping user-accessible IDs to internal IDs). Log authorization failures for security monitoring. Test authorization with different user roles and edge cases. Never rely solely on client-side access controls.

Common Consequences

ImpactDetails
Access ControlScope: Unauthorized Access

Users can access resources and data belonging to other users or higher-privileged accounts.
IntegrityScope: Data Modification

Attackers can modify or delete data they shouldn't have access to.
ConfidentialityScope: Data Exposure

Sensitive information is exposed to unauthorized users through IDOR vulnerabilities.

Example Code + Solution Code

Vulnerable Code

# VULNERABLE: No authorization check - classic IDOR
@app.route('/api/documents/<doc_id>')
@login_required
def get_document(doc_id):
    # Only checks if user is logged in, not if they own the document!
    document = Document.query.get(doc_id)
    return jsonify(document.to_dict())

# VULNERABLE: No authorization on delete
@app.route('/api/users/<user_id>', methods=['DELETE'])
@login_required
def delete_user(user_id):
    # Any authenticated user can delete any other user!
    user = User.query.get(user_id)
    db.session.delete(user)
    db.session.commit()
    return 'Deleted'

# VULNERABLE: Admin functionality without role check
@app.route('/admin/users')
@login_required  # Only checks authentication, not authorization!
def list_all_users():
    users = User.query.all()
    return jsonify([u.to_dict() for u in users])
// VULNERABLE: Missing ownership check
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/{orderId}")
    public Order getOrder(@PathVariable Long orderId,
                         @AuthenticationPrincipal User user) {
        // User is authenticated but order ownership not verified!
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new NotFoundException("Order not found"));
    }

    @DeleteMapping("/{orderId}")
    public void deleteOrder(@PathVariable Long orderId) {
        // No authentication or authorization at all!
        orderRepository.deleteById(orderId);
    }
}
// VULNERABLE: Direct object reference without authorization
app.get('/api/profile/:userId', authenticate, (req, res) => {
    // authenticate middleware only verifies token is valid
    // doesn't check if user can access this profile
    const user = await db.users.findById(req.params.userId);
    res.json(user);  // Returns any user's profile!
});

// VULNERABLE: Missing authorization on sensitive action
app.post('/api/transfer', authenticate, (req, res) => {
    const { fromAccount, toAccount, amount } = req.body;
    // Doesn't verify user owns fromAccount!
    transferFunds(fromAccount, toAccount, amount);
    res.json({ success: true });
});

Fixed Code

# SAFE: Authorization check on document access
@app.route('/api/documents/<doc_id>')
@login_required
def get_document_safe(doc_id):
    document = Document.query.get_or_404(doc_id)

    # Verify current user owns the document or has access
    if document.owner_id != current_user.id and \
       not document.has_viewer(current_user.id):
        abort(403, 'Access denied')

    return jsonify(document.to_dict())

# SAFE: Authorization on delete - only owner or admin
@app.route('/api/users/<user_id>', methods=['DELETE'])
@login_required
def delete_user_safe(user_id):
    # Only allow self-deletion or admin deletion
    if current_user.id != int(user_id) and not current_user.is_admin:
        abort(403, 'Access denied')

    user = User.query.get_or_404(user_id)

    # Prevent deleting other admins
    if user.is_admin and current_user.id != int(user_id):
        abort(403, 'Cannot delete other administrators')

    db.session.delete(user)
    db.session.commit()

    audit_log.info(f"User {user_id} deleted by {current_user.id}")
    return 'Deleted'

# SAFE: Role-based authorization for admin functionality
@app.route('/admin/users')
@login_required
@require_role('admin')  # Decorator checks user role
def list_all_users_safe():
    audit_log.info(f"Admin {current_user.id} listed all users")
    users = User.query.all()
    return jsonify([u.to_safe_dict() for u in users])

def require_role(role):
    def decorator(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            if not current_user.has_role(role):
                abort(403, f'Requires {role} role')
            return f(*args, **kwargs)
        return decorated
    return decorator
// SAFE: Ownership verification on resource access
@RestController
@RequestMapping("/api/orders")
public class SecureOrderController {

    @GetMapping("/{orderId}")
    @PreAuthorize("@orderSecurity.isOwner(#orderId, authentication.principal)")
    public Order getOrder(@PathVariable Long orderId,
                         @AuthenticationPrincipal User user) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new NotFoundException("Order not found"));
    }

    @DeleteMapping("/{orderId}")
    @PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#orderId, authentication.principal)")
    public void deleteOrder(@PathVariable Long orderId,
                           @AuthenticationPrincipal User user) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new NotFoundException("Order not found"));

        auditService.log("ORDER_DELETED", user.getId(), orderId);
        orderRepository.delete(order);
    }
}

@Component
public class OrderSecurity {
    @Autowired
    private OrderRepository orderRepository;

    public boolean isOwner(Long orderId, User user) {
        return orderRepository.findById(orderId)
            .map(order -> order.getUserId().equals(user.getId()))
            .orElse(false);
    }
}
// SAFE: Authorization middleware and ownership checks
const authorizeResourceAccess = (resourceType) => {
    return async (req, res, next) => {
        const resourceId = req.params[`${resourceType}Id`] || req.params.id;
        const userId = req.user.id;

        const resource = await db[resourceType].findById(resourceId);

        if (!resource) {
            return res.status(404).json({ error: 'Not found' });
        }

        // Check ownership or admin role
        if (resource.ownerId !== userId && !req.user.roles.includes('admin')) {
            auditLog.warn(`Unauthorized access attempt: user ${userId} to ${resourceType} ${resourceId}`);
            return res.status(403).json({ error: 'Access denied' });
        }

        req.resource = resource;
        next();
    };
};

app.get('/api/profile/:userId',
    authenticate,
    authorizeResourceAccess('users'),
    (req, res) => {
        // Only reached if user is authorized
        res.json(req.resource.toSafeJSON());
    }
);

// SAFE: Account ownership verification for transfers
app.post('/api/transfer',
    authenticate,
    async (req, res) => {
        const { fromAccount, toAccount, amount } = req.body;

        // Verify user owns the source account
        const account = await db.accounts.findById(fromAccount);

        if (!account || account.ownerId !== req.user.id) {
            return res.status(403).json({ error: 'Not authorized for this account' });
        }

        await transferFunds(fromAccount, toAccount, amount);

        auditLog.info(`Transfer: ${amount} from ${fromAccount} to ${toAccount} by user ${req.user.id}`);
        res.json({ success: true });
    }
);

Exploited in the Wild

IBM i SQL Services Privilege Escalation (IBM, 2025)

CVE-2025-36367 in IBM i versions 7.2-7.6 contains an invalid authorization check in SQL services that allows authenticated attackers to escalate privileges to root on the host operating system.

IDonate WordPress Plugin User Deletion (IDonate, 2025)

CVE-2025-11154 in IDonate WordPress plugin before 2.1.13 allows unauthenticated attackers to delete arbitrary user accounts due to missing authorization checks on the user deletion handler.

PostX WordPress Plugin Password Hash Exposure (PostX, 2025)

CVE-2025-12980 (CVSS 7.5) in PostX WordPress plugin allows unauthenticated users to retrieve user metadata including password hashes through an unprotected REST API endpoint.


Tools to test/exploit

  • Burp Suite — test IDOR by manipulating object IDs.

  • Autorize — Burp extension for authorization testing.

  • OWASP ZAP — automated authorization testing.


CVE Examples


References

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

  2. OWASP. "Broken Access Control." https://owasp.org/Top10/A01_2021-Broken_Access_Control/