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
| Impact | Details |
|---|---|
| Access Control | Scope: Unauthorized Access Users can access resources and data belonging to other users or higher-privileged accounts. |
| Integrity | Scope: Data Modification Attackers can modify or delete data they shouldn't have access to. |
| Confidentiality | Scope: 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
-
CVE-2025-36367 — IBM i SQL services privilege escalation.
-
CVE-2025-11154 — IDonate user deletion without authorization.
-
CVE-2025-12980 — PostX password hash exposure.
References
-
MITRE. "CWE-862: Missing Authorization." https://cwe.mitre.org/data/definitions/862.html
-
OWASP. "Broken Access Control." https://owasp.org/Top10/A01_2021-Broken_Access_Control/