Not Using Complete Mediation

Description

Not Using Complete Mediation occurs when a system fails to check authorization for every access to a protected resource. Instead of verifying permissions on each request, the system may only check once (at login, first access, or cached result) and assume subsequent accesses are authorized. This allows attackers to bypass access controls after initial verification or through direct object references.

Risk

Users can access resources after their permissions are revoked. Session hijacking grants full access without re-verification. Direct URL access bypasses navigation-based security. Cached authorization decisions become stale. Race conditions between permission changes and access. Horizontal and vertical privilege escalation through direct references.

Solution

Verify authorization on every access request. Never cache authorization decisions permanently. Implement server-side authorization checks independent of client navigation. Use centralized authorization service. Re-validate permissions for sensitive operations. Audit all access to protected resources.

Common Consequences

ImpactDetails
AuthorizationScope: Bypass

Access controls circumvented after initial check.
ConfidentialityScope: Data Exposure

Unauthorized access to protected data.
IntegrityScope: Unauthorized Modification

Changes made without proper authorization.

Example Code + Solution Code

Vulnerable Code

// VULNERABLE: One-time authorization check
public class VulnerableDocumentService {

    private Map<String, Boolean> authorizedUsers = new HashMap<>();

    public void login(String userId) {
        // Check authorization only at login
        boolean hasAccess = authService.checkDocumentAccess(userId);
        authorizedUsers.put(userId, hasAccess);  // Cached forever!
    }

    public Document getDocument(String userId, String docId) {
        // Uses cached authorization - never re-checks!
        if (authorizedUsers.getOrDefault(userId, false)) {
            return documentRepository.findById(docId);
        }
        throw new UnauthorizedException();
    }

    // Permission changes not reflected until re-login!
}

// VULNERABLE: No authorization check on direct access
@RestController
public class VulnerableFileController {

    @GetMapping("/files/{fileId}")
    public byte[] getFile(@PathVariable String fileId) {
        // No authorization check!
        // Anyone with the URL can access any file
        return fileService.getFileContent(fileId);
    }

    @GetMapping("/admin/users")
    public List<User> listUsers() {
        // Assumes only admins know this URL
        // No actual admin verification!
        return userService.findAll();
    }
}

// VULNERABLE: Client-side navigation as only protection
public class VulnerableAdminPanel {

    public void showAdminMenu(User user) {
        if (user.isAdmin()) {
            // Show admin menu items
        }
        // Actual endpoints have no checks!
    }

    // Admin functions accessible to anyone who knows the URL
    public void deleteUser(String userId) {
        userRepository.delete(userId);  // No auth check!
    }
}
# VULNERABLE: Session-based caching without re-validation
class VulnerableAccessControl:
    def __init__(self):
        self.permission_cache = {}

    def login(self, user_id):
        # Cache permissions at login
        permissions = self.db.get_permissions(user_id)
        self.permission_cache[user_id] = permissions
        # Never refreshed!

    def check_access(self, user_id, resource):
        # Uses stale cached permissions
        permissions = self.permission_cache.get(user_id, [])
        return resource in permissions

# VULNERABLE: First-access-only checking
class VulnerableResourceManager:
    def __init__(self):
        self.accessed_resources = set()

    def get_resource(self, user_id, resource_id):
        # Only check on first access
        if resource_id not in self.accessed_resources:
            if not self.auth_service.check(user_id, resource_id):
                raise PermissionError()
            self.accessed_resources.add(resource_id)
            # Subsequent accesses skip check!

        return self.load_resource(resource_id)

# VULNERABLE: No mediation on API endpoints
from flask import Flask, request
app = Flask(__name__)

@app.route('/api/users/<user_id>/data')
def get_user_data(user_id):
    # No check if requester can access this user's data!
    # Anyone can access any user's data
    return jsonify(db.get_user_data(user_id))

@app.route('/api/documents/<doc_id>')
def get_document(doc_id):
    # Direct object reference without authorization
    return send_file(f'documents/{doc_id}')
// VULNERABLE: One-time role check
class VulnerableAuthMiddleware {
    constructor() {
        this.authorizedSessions = new Map();
    }

    async onLogin(req, res, next) {
        const user = await this.authenticate(req.body);
        if (user) {
            // Store authorization decision
            this.authorizedSessions.set(req.sessionId, user.role);
        }
    }

    // Middleware uses cached role
    checkRole(requiredRole) {
        return (req, res, next) => {
            const role = this.authorizedSessions.get(req.sessionId);
            // Uses cached role - not current!
            if (role === requiredRole) {
                next();
            } else {
                res.status(403).send('Forbidden');
            }
        };
    }
}

// VULNERABLE: Missing authorization on routes
app.get('/users/:userId/profile', async (req, res) => {
    // No check if current user can view this profile!
    const profile = await db.getUserProfile(req.params.userId);
    res.json(profile);
});

app.delete('/posts/:postId', async (req, res) => {
    // No check if current user owns this post!
    await db.deletePost(req.params.postId);
    res.json({ deleted: true });
});
<?php
// VULNERABLE: Session-only authorization
session_start();

function checkLoginOnce() {
    if ($_SESSION['logged_in']) {
        return true;  // Trust session forever
    }
    return false;
}

// All admin pages use same weak check
function adminPage() {
    if (!$_SESSION['is_admin']) {  // Set once at login
        die('Access denied');
    }
    // Admin could have been demoted but still has access
}

// VULNERABLE: Direct file access
$file = $_GET['file'];
// No authorization check!
readfile("/documents/$file");

// VULNERABLE: No authorization per request
function updateProfile($userId, $data) {
    // Assumes caller already verified they own this profile
    $db->update('users', $data, ['id' => $userId]);
}

// Anyone can call:
updateProfile($_GET['userId'], $_POST['data']);
?>

Fixed Code

// SAFE: Complete mediation on every access
@Service
public class SafeDocumentService {

    private final AuthorizationService authService;
    private final DocumentRepository documentRepository;

    public Document getDocument(String userId, String docId) {
        // Always verify authorization
        if (!authService.canAccessDocument(userId, docId)) {
            throw new UnauthorizedException("Access denied to document");
        }

        return documentRepository.findById(docId);
    }

    public void updateDocument(String userId, String docId, String content) {
        // Re-check even for updates
        if (!authService.canModifyDocument(userId, docId)) {
            throw new UnauthorizedException("Cannot modify document");
        }

        documentRepository.update(docId, content);
    }
}

// SAFE: Authorization on every endpoint
@RestController
public class SafeFileController {

    @GetMapping("/files/{fileId}")
    public byte[] getFile(@AuthenticationPrincipal User user,
                          @PathVariable String fileId) {
        // Always verify access
        if (!fileService.canUserAccess(user.getId(), fileId)) {
            throw new AccessDeniedException("Cannot access file");
        }

        return fileService.getFileContent(fileId);
    }

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin/users")
    public List<User> listUsers() {
        // Spring Security verifies role on every request
        return userService.findAll();
    }
}

// SAFE: Centralized authorization service
@Service
public class AuthorizationService {

    public boolean canAccessDocument(String userId, String docId) {
        // Always fetch current permissions from database
        User user = userRepository.findById(userId);
        Document doc = documentRepository.findById(docId);

        // Check ownership
        if (doc.getOwnerId().equals(userId)) {
            return true;
        }

        // Check shared access
        return shareRepository.hasAccess(userId, docId);
    }
}
# SAFE: Complete mediation with fresh permission checks
class SafeAccessControl:
    def __init__(self, db):
        self.db = db

    def check_access(self, user_id: str, resource_id: str, action: str) -> bool:
        """Always fetch current permissions - never cache."""
        # Get current user permissions from database
        permissions = self.db.get_current_permissions(user_id)

        # Check resource-specific access
        return self._evaluate_permissions(permissions, resource_id, action)

# SAFE: Decorator for complete mediation
from functools import wraps

def require_access(resource_type):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            user_id = get_current_user_id()
            resource_id = kwargs.get('resource_id') or args[0]

            # Always check on every request
            if not auth_service.check_access(user_id, resource_type, resource_id):
                raise PermissionError(f"Access denied to {resource_type}")

            return f(*args, **kwargs)
        return wrapper
    return decorator

@app.route('/api/users/<user_id>/data')
@require_access('user_data')
def get_user_data(user_id):
    # Access already verified by decorator
    return jsonify(db.get_user_data(user_id))

@app.route('/api/documents/<doc_id>')
@require_access('document')
def get_document(doc_id):
    return send_file(f'documents/{doc_id}')

# SAFE: Object-level authorization
class SafeResourceManager:
    def get_resource(self, user_id: str, resource_id: str):
        # Check ownership/access every time
        resource = self.db.get_resource(resource_id)

        if resource.owner_id != user_id:
            if not self.db.has_shared_access(user_id, resource_id):
                raise PermissionError("Access denied")

        return resource
// SAFE: Middleware with complete mediation
class SafeAuthMiddleware {
    constructor(authService) {
        this.authService = authService;
    }

    // Check on every request
    authorize(requiredPermission) {
        return async (req, res, next) => {
            // Always fetch current permissions
            const hasPermission = await this.authService.checkPermission(
                req.user.id,
                requiredPermission,
                req.params
            );

            if (!hasPermission) {
                return res.status(403).json({ error: 'Access denied' });
            }

            next();
        };
    }
}

// SAFE: Complete mediation on all routes
const auth = new SafeAuthMiddleware(authService);

app.get('/users/:userId/profile',
    auth.authorize('read:user_profile'),
    async (req, res) => {
        // Additional ownership check
        if (req.user.id !== req.params.userId &&
            !req.user.roles.includes('admin')) {
            return res.status(403).json({ error: 'Access denied' });
        }

        const profile = await db.getUserProfile(req.params.userId);
        res.json(profile);
    }
);

app.delete('/posts/:postId',
    auth.authorize('delete:post'),
    async (req, res) => {
        const post = await db.getPost(req.params.postId);

        // Verify ownership
        if (post.authorId !== req.user.id) {
            return res.status(403).json({ error: 'Not your post' });
        }

        await db.deletePost(req.params.postId);
        res.json({ deleted: true });
    }
);
<?php
// SAFE: Authorization service with complete mediation
class AuthorizationService {
    private $db;

    public function __construct($db) {
        $this->db = $db;
    }

    public function checkAccess($userId, $resource, $action) {
        // Always query current permissions
        $permissions = $this->db->query(
            "SELECT * FROM permissions WHERE user_id = ? AND resource = ?",
            [$userId, $resource]
        )->fetch();

        if (!$permissions) {
            return false;
        }

        return in_array($action, explode(',', $permissions['actions']));
    }

    public function requireAccess($userId, $resource, $action) {
        if (!$this->checkAccess($userId, $resource, $action)) {
            http_response_code(403);
            die(json_encode(['error' => 'Access denied']));
        }
    }
}

// SAFE: Every endpoint checks authorization
$auth = new AuthorizationService($db);
$currentUserId = $_SESSION['user_id'];

// Document access
$docId = $_GET['doc_id'];
$auth->requireAccess($currentUserId, "document:$docId", 'read');
$doc = $db->getDocument($docId);

// Profile update - verify ownership
$profileId = $_POST['profile_id'];
if ($profileId !== $currentUserId) {
    $auth->requireAccess($currentUserId, 'all_profiles', 'edit');
}
updateProfile($profileId, $_POST['data']);

// Admin action - verify current admin status
$auth->requireAccess($currentUserId, 'admin_panel', 'access');
// Admin functionality here
?>

Exploited in the Wild

IDOR Vulnerabilities

Direct object references without authorization checks.

Admin Panel Access

URLs discoverable without proper access control.

Session Fixation

Session remains authorized after permission revocation.


Tools to test/exploit

  • Burp Suite — test authorization on all endpoints.

  • OWASP ZAP automated scanning.

  • Manual testing of direct object references.


CVE Examples

  • Numerous IDOR (Insecure Direct Object Reference) CVEs.

  • Admin bypass through URL guessing.


References

  1. MITRE. "CWE-638: Not Using Complete Mediation." https://cwe.mitre.org/data/definitions/638.html

  2. OWASP. "Broken Access Control."