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
| Impact | Details |
|---|---|
| Authorization | Scope: Bypass Access controls circumvented after initial check. |
| Confidentiality | Scope: Data Exposure Unauthorized access to protected data. |
| Integrity | Scope: 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
-
MITRE. "CWE-638: Not Using Complete Mediation." https://cwe.mitre.org/data/definitions/638.html
-
OWASP. "Broken Access Control."