Authorization Vulnerabilities Overview
Authorization Vulnerabilities Overview
Authorization is the process of determining whether an authenticated user has permission to access a resource or perform an action. Authorization vulnerabilities arise when these permission checks are missing, incomplete, or can be bypassed.These vulnerabilities can allow attackers to access unauthorized data, perform privileged operations, or completely take over accounts and systems. Proper authorization is a critical component of a secure application’s defense-in-depth strategy.
Missing Function Level Authorization
Missing Function Level Authorization
// Anti-pattern: Missing function level authorization
app.post('/api/users/:id/delete', (req, res) => {
const userId = req.params.id;
db.deleteUser(userId)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
// Better approach: Implementing function level authorization
app.post('/api/users/:id/delete', authenticateUser, authorizeAdmin, (req, res) => {
const userId = req.params.id;
db.deleteUser(userId)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
// Authorization middleware
function authorizeAdmin(req, res, next) {
if (req.user && req.user.role === 'admin') {
next();
} else {
res.status(403).json({ error: 'Unauthorized access' });
}
}
- Apply authorization checks to all sensitive functions
- Use role-based access control (RBAC) or attribute-based access control (ABAC)
- Implement middleware for common authorization patterns
- Apply the principle of least privilege
- Log authorization decisions for sensitive operations
Insecure Direct Object References (IDOR)
Insecure Direct Object References (IDOR)
// Anti-pattern: Insecure direct object references
app.get('/api/documents/:id', (req, res) => {
const documentId = req.params.id;
db.getDocument(documentId)
.then(document => res.json(document))
.catch(err => res.status(500).json({ error: err.message }));
});
// Better approach: Checking ownership or permissions
app.get('/api/documents/:id', authenticateUser, (req, res) => {
const documentId = req.params.id;
const userId = req.user.id;
db.getDocument(documentId)
.then(document => {
if (document.ownerId === userId || hasAccess(userId, document)) {
res.json(document);
} else {
res.status(403).json({ error: 'Access denied' });
}
})
.catch(err => res.status(500).json({ error: err.message }));
});
- Implement proper access control checks for each object access
- Use indirect references that are mapped to actual database IDs
- Validate that the current user has permission to access the requested object
- Implement proper error handling that doesn’t reveal sensitive information
- Use database queries that include user permissions in the selection criteria
Broken Object Level Authorization
Broken Object Level Authorization
// Anti-pattern: Broken object level authorization
app.put('/api/accounts/:accountId', authenticateUser, (req, res) => {
const accountId = req.params.accountId;
const updates = req.body;
db.updateAccount(accountId, updates)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
// Better approach: Proper object level authorization
app.put('/api/accounts/:accountId', authenticateUser, (req, res) => {
const accountId = req.params.accountId;
const userId = req.user.id;
const updates = req.body;
// Check if user has access to this account
db.getAccount(accountId)
.then(account => {
if (account.userId !== userId && !isAdmin(req.user)) {
return res.status(403).json({ error: 'Access denied' });
}
return db.updateAccount(accountId, updates);
})
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
- Verify ownership or permission for every object access
- Implement a consistent authorization model across all objects
- Use database queries that include user permissions
- Consider using an authorization framework
- Test authorization logic thoroughly with different user roles
Privilege Escalation
Privilege Escalation
// Anti-pattern: Vulnerable to privilege escalation
app.post('/api/users/:id/update', authenticateUser, (req, res) => {
const userId = req.params.id;
const updates = req.body;
// No check if the authenticated user can modify this user
db.updateUser(userId, updates)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
// Better approach: Preventing privilege escalation
app.post('/api/users/:id/update', authenticateUser, (req, res) => {
const targetUserId = req.params.id;
const currentUserId = req.user.id;
const updates = req.body;
// Check if user is updating their own profile or is an admin
if (targetUserId !== currentUserId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
// Prevent role escalation
if (updates.role && req.user.role !== 'admin') {
delete updates.role;
}
db.updateUser(targetUserId, updates)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
- Implement strict role checks for sensitive operations
- Prevent users from modifying their own roles or permissions
- Validate that users can only access or modify their own data unless explicitly authorized
- Implement proper access control for administrative functions
- Log and monitor privilege changes
Missing Authorization Headers
Missing Authorization Headers
// Anti-pattern: Missing authorization headers in API requests
function fetchUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json());
}
// Better approach: Including authorization headers
function fetchUserData(userId) {
const token = getAuthToken();
return fetch(`/api/users/${userId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
}).then(response => response.json());
}
- Always include authorization tokens in API requests
- Implement consistent token validation on the server
- Use secure methods for storing and transmitting tokens
- Implement token expiration and refresh mechanisms
- Consider using standardized authentication schemes like OAuth 2.0 or JWT
Improper Access Control
Improper Access Control
// Anti-pattern: Improper access control
@RequestMapping("/api/users/{id}")
public User getUserDetails(@PathVariable Long id) {
return userRepository.findById(id).orElse(null);
}
// Better approach: Proper access control
@RequestMapping("/api/users/{id}")
public ResponseEntity<?> getUserDetails(@PathVariable Long id, Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
User currentUser = userRepository.findByUsername(userDetails.getUsername());
// Check if user is requesting their own data or is an admin
if (currentUser.getId().equals(id) || currentUser.getRole().equals("ADMIN")) {
User requestedUser = userRepository.findById(id).orElse(null);
if (requestedUser != null) {
return ResponseEntity.ok(requestedUser);
}
return ResponseEntity.notFound().build();
}
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Access denied");
}
- Verify user permissions for every protected resource
- Implement role-based or attribute-based access control
- Apply the principle of least privilege
- Use declarative security when possible
- Centralize access control logic
- Regularly audit access control implementations
JWT Without Signature Verification
JWT Without Signature Verification
// Anti-pattern: Not verifying JWT signature
app.get('/api/protected', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
// Dangerous: Decoding JWT without verifying signature
const decoded = jwt.decode(token);
if (!decoded) {
return res.status(401).json({ error: 'Invalid token' });
}
// Using the unverified token payload
req.user = decoded;
// Process the request...
});
// Better approach: Properly verifying JWT signature
app.get('/api/protected', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
// Verify the token with the secret key
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
// Process the request...
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
});
- Always verify JWT signatures using the appropriate algorithm and key
- Validate all JWT claims (expiration, issuer, audience, etc.)
- Use strong, properly secured signing keys
- Implement token expiration and refresh mechanisms
- Consider using a JWT library that handles security best practices
Relying on Client-Side Authorization
Relying on Client-Side Authorization
// Anti-pattern: Relying on client-side authorization
// Client-side code
function showAdminControls() {
if (currentUser.isAdmin) {
document.getElementById('adminPanel').style.display = 'block';
}
}
// API endpoint with no server-side checks
app.delete('/api/users/:id', (req, res) => {
const userId = req.params.id;
db.deleteUser(userId)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
// Better approach: Server-side authorization
// Client-side code remains the same for UI purposes
function showAdminControls() {
if (currentUser.isAdmin) {
document.getElementById('adminPanel').style.display = 'block';
}
}
// API endpoint with proper server-side checks
app.delete('/api/users/:id', authenticateUser, (req, res) => {
// Server-side verification of admin status
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
const userId = req.params.id;
db.deleteUser(userId)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
- Always enforce authorization on the server side
- Treat client-side authorization as a UI convenience only
- Implement consistent authorization checks across all API endpoints
- Validate all incoming requests regardless of the client
- Test API endpoints directly to ensure proper authorization
Insufficient Authorization Granularity
Insufficient Authorization Granularity
// Anti-pattern: Insufficient authorization granularity
app.get('/api/reports', authenticateUser, (req, res) => {
// Only checks if user is authenticated, not what reports they can access
db.getAllReports()
.then(reports => res.json(reports))
.catch(err => res.status(500).json({ error: err.message }));
});
// Better approach: Fine-grained authorization
app.get('/api/reports', authenticateUser, (req, res) => {
const userId = req.user.id;
const userRole = req.user.role;
// Get reports based on user's role and permissions
if (userRole === 'admin') {
// Admins can see all reports
db.getAllReports()
.then(reports => res.json(reports))
.catch(err => res.status(500).json({ error: err.message }));
} else if (userRole === 'manager') {
// Managers can see reports for their department
db.getReportsByDepartment(req.user.department)
.then(reports => res.json(reports))
.catch(err => res.status(500).json({ error: err.message }));
} else {
// Regular users can only see their own reports
db.getReportsByUser(userId)
.then(reports => res.json(reports))
.catch(err => res.status(500).json({ error: err.message }));
}
});
- Define specific permissions for different resources and actions
- Implement role-based access control with detailed permission sets
- Consider attribute-based access control for complex authorization requirements
- Apply data filtering based on user permissions
- Regularly review and refine authorization rules
Hardcoded Roles or Permissions
Hardcoded Roles or Permissions
// Anti-pattern: Hardcoded roles or permissions
@RequestMapping("/admin/dashboard")
public String adminDashboard(Model model, Principal principal) {
User user = userService.findByUsername(principal.getName());
// Hardcoded role check
if ("admin".equals(user.getRole())) {
// Show admin dashboard
return "admin/dashboard";
}
return "redirect:/access-denied";
}
// Better approach: Using a permission system
@RequestMapping("/admin/dashboard")
@PreAuthorize("hasAuthority('ADMIN_DASHBOARD_ACCESS')")
public String adminDashboard(Model model) {
// Method is only executed if user has the required permission
return "admin/dashboard";
}
- Use a configurable permission system
- Implement declarative security with annotations or configuration
- Store permissions in a database or configuration file
- Use role hierarchies or permission inheritance when appropriate
- Implement a centralized authorization service
Missing Re-Authentication for Sensitive Operations
Missing Re-Authentication for Sensitive Operations
// Anti-pattern: No re-authentication for sensitive operations
app.post('/api/users/change-password', authenticateUser, (req, res) => {
const userId = req.user.id;
const newPassword = req.body.newPassword;
// Change password without requiring current password
updatePassword(userId, newPassword)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
// Better approach: Requiring re-authentication
app.post('/api/users/change-password', authenticateUser, (req, res) => {
const userId = req.user.id;
const currentPassword = req.body.currentPassword;
const newPassword = req.body.newPassword;
// Verify current password before allowing change
verifyPassword(userId, currentPassword)
.then(isValid => {
if (!isValid) {
return res.status(401).json({ error: 'Current password is incorrect' });
}
return updatePassword(userId, newPassword);
})
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
- Require password verification for sensitive operations
- Implement step-up authentication for high-risk actions
- Consider using multi-factor authentication for critical operations
- Implement proper session management
- Log all sensitive operations
Insecure Authorization Decisions
Insecure Authorization Decisions
// Anti-pattern: Insecure authorization decisions
app.get('/api/documents/:id', authenticateUser, (req, res) => {
const documentId = req.params.id;
// Insecure: Using user-supplied data for authorization decision
if (req.query.role === 'admin') {
// Process as admin
db.getDocument(documentId)
.then(document => res.json(document))
.catch(err => res.status(500).json({ error: err.message }));
} else {
// Limited access
db.getDocumentLimited(documentId, req.user.id)
.then(document => res.json(document))
.catch(err => res.status(500).json({ error: err.message }));
}
});
// Better approach: Secure authorization decisions
app.get('/api/documents/:id', authenticateUser, (req, res) => {
const documentId = req.params.id;
// Secure: Using server-verified user data for authorization
getUserRole(req.user.id)
.then(role => {
if (role === 'admin') {
// Process as admin
return db.getDocument(documentId);
} else {
// Limited access
return db.getDocumentLimited(documentId, req.user.id);
}
})
.then(document => res.json(document))
.catch(err => res.status(500).json({ error: err.message }));
});
- Base authorization decisions only on server-verified data
- Never trust client-supplied role or permission information
- Implement proper session management
- Use secure, tamper-proof tokens for authorization
- Validate all inputs used in authorization decisions
Lack of Context-Aware Authorization
Lack of Context-Aware Authorization
// Anti-pattern: Lack of context-aware authorization
app.post('/api/transfer', authenticateUser, (req, res) => {
const { fromAccount, toAccount, amount } = req.body;
// Only checks if user is authenticated, not contextual factors
transferFunds(fromAccount, toAccount, amount)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
// Better approach: Context-aware authorization
app.post('/api/transfer', authenticateUser, (req, res) => {
const { fromAccount, toAccount, amount } = req.body;
const userId = req.user.id;
// Check account ownership
isAccountOwner(userId, fromAccount)
.then(isOwner => {
if (!isOwner) {
return res.status(403).json({ error: 'Unauthorized' });
}
// Check transfer limits based on user's verification level
return getUserVerificationLevel(userId);
})
.then(verificationLevel => {
const transferLimit = getTransferLimit(verificationLevel);
if (amount > transferLimit) {
return res.status(403).json({
error: `Transfer amount exceeds limit for your verification level (${transferLimit})`
});
}
// Check for suspicious activity
return isTransferSuspicious(userId, fromAccount, toAccount, amount);
})
.then(isSuspicious => {
if (isSuspicious) {
// Require additional verification
return res.status(403).json({
error: 'Additional verification required for this transfer',
requiresVerification: true
});
}
return transferFunds(fromAccount, toAccount, amount);
})
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
- Consider factors like time, location, device, and behavior patterns
- Implement risk-based authentication for sensitive operations
- Apply different authorization rules based on the context
- Use anomaly detection for suspicious activities
- Implement step-up authentication when context changes
Failure to Validate Authorization on Each Request
Failure to Validate Authorization on Each Request
// Anti-pattern: Validating authorization only once
// Initial login and token generation
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
authenticateUser(username, password)
.then(user => {
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate token with user roles and permissions
const token = jwt.sign(
{ id: user.id, role: user.role, permissions: user.permissions },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({ token });
})
.catch(err => res.status(500).json({ error: err.message }));
});
// API endpoint that only checks token validity, not current permissions
app.delete('/api/users/:id', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Only checks permissions in the token, not current permissions
if (decoded.role !== 'admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
const userId = req.params.id;
db.deleteUser(userId)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
});
// Better approach: Validating current permissions on each request
app.delete('/api/users/:id', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.id;
// Check current permissions from the database
getCurrentUserPermissions(userId)
.then(permissions => {
if (!permissions.includes('user:delete')) {
return res.status(403).json({ error: 'Unauthorized' });
}
const targetUserId = req.params.id;
return db.deleteUser(targetUserId);
})
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
});
- Validate permissions on every request
- Consider using short-lived tokens
- Implement a token revocation mechanism
- Check current permissions from the database when needed
- Use a centralized authorization service