Improper Access Control

Description

Improper Access Control occurs when software does not properly restrict access to a resource from an unauthorized actor. This is a broad category that encompasses many specific access control failures including authentication bypass, authorization bypass, and improper enforcement of access policies. Access control involves determining who can access what resources and what actions they can perform. Failures can occur in authentication (who you are), authorization (what you can do), or accounting (tracking what was done).

Risk

Access control failures are consistently among the most critical security vulnerabilities. They directly lead to unauthorized access to sensitive data, unauthorized modification of systems, and potential complete system compromise. OWASP ranks Broken Access Control as the #1 vulnerability category. Real-world impacts include data breaches exposing millions of records, financial fraud, and system takeovers. Access control bugs are particularly dangerous because they often require no exploitation skills—just knowledge of the flaw.

Solution

Implement the principle of least privilege. Centralize access control logic and avoid duplicating authorization checks. Deny by default—require explicit grants. Validate authorization on every request at the server side. Use established access control frameworks. Implement role-based access control (RBAC) or attribute-based access control (ABAC). Log access control failures for monitoring. Conduct regular access control reviews and testing.

Common Consequences

ImpactDetails
ConfidentialityScope: Data Exposure

Unauthorized access to sensitive data including personal information, financial data, and trade secrets.
IntegrityScope: Data Modification

Unauthorized ability to modify, delete, or corrupt data and system configurations.
AvailabilityScope: Denial of Service

Unauthorized access can be used to disrupt services or destroy data.

Example Code + Solution Code

Vulnerable Code

# VULNERABLE: Missing access control
@app.route('/admin/users')
def list_all_users():
    # No authentication or authorization check!
    return jsonify(User.query.all())

# VULNERABLE: Insecure direct object reference (IDOR)
@app.route('/api/invoice/<invoice_id>')
@login_required
def get_invoice(invoice_id):
    # Any logged-in user can access any invoice
    invoice = Invoice.query.get(invoice_id)
    return jsonify(invoice.to_dict())

# VULNERABLE: Horizontal privilege escalation
@app.route('/api/user/<user_id>/settings', methods=['PUT'])
@login_required
def update_settings(user_id):
    # User can update anyone's settings by changing user_id
    user = User.query.get(user_id)
    user.settings = request.json
    db.session.commit()
    return 'Updated'

# VULNERABLE: Vertical privilege escalation
@app.route('/api/user/<user_id>/promote', methods=['POST'])
@login_required
def promote_user(user_id):
    # No admin check - any user can promote anyone!
    user = User.query.get(user_id)
    user.role = 'admin'
    db.session.commit()
    return 'Promoted'
// VULNERABLE: Path traversal bypass
@GetMapping("/files/{filename}")
public ResponseEntity<Resource> getFile(@PathVariable String filename) {
    // No validation - attacker uses ../../../etc/passwd
    Path filePath = Paths.get("/uploads/" + filename);
    Resource resource = new FileSystemResource(filePath);
    return ResponseEntity.ok(resource);
}

// VULNERABLE: Parameter manipulation
@PostMapping("/transfer")
public ResponseEntity<?> transfer(@RequestBody TransferRequest request) {
    // Trusts 'fromAccount' from request - should use authenticated user's account
    accountService.transfer(
        request.getFromAccount(),  // Attacker specifies victim's account!
        request.getToAccount(),
        request.getAmount()
    );
    return ResponseEntity.ok().build();
}

// VULNERABLE: Missing function-level access control
@DeleteMapping("/users/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
    // No check if user has delete permission
    userRepository.deleteById(id);
    return ResponseEntity.ok().build();
}
// VULNERABLE: Client-side only access control
// Frontend hides admin menu but backend doesn't enforce
app.get('/api/admin/config', (req, res) => {
    // No server-side check - anyone can access!
    res.json(systemConfig);
});

// VULNERABLE: Predictable resource IDs
app.get('/api/documents/:id', async (req, res) => {
    // Sequential IDs make enumeration easy
    // No ownership check
    const doc = await Document.findById(req.params.id);
    res.json(doc);
});

// VULNERABLE: Mass assignment
app.put('/api/profile', async (req, res) => {
    // User can set any field including 'role' or 'isAdmin'
    await User.findByIdAndUpdate(req.userId, req.body);
    res.json({ success: true });
});

Fixed Code

# SAFE: Proper access control with authentication and authorization
from functools import wraps

def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not current_user.is_authenticated:
            abort(401)
        if not current_user.has_role('admin'):
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

@app.route('/admin/users')
@admin_required
def list_all_users():
    return jsonify([u.to_dict() for u in User.query.all()])

# SAFE: Resource ownership verification
@app.route('/api/invoice/<invoice_id>')
@login_required
def get_invoice(invoice_id):
    invoice = Invoice.query.get_or_404(invoice_id)

    # Verify ownership or admin access
    if invoice.user_id != current_user.id and not current_user.is_admin:
        abort(403, 'Access denied')

    return jsonify(invoice.to_dict())

# SAFE: Prevent horizontal privilege escalation
@app.route('/api/user/settings', methods=['PUT'])
@login_required
def update_settings():
    # Always use authenticated user's ID, not from request
    current_user.settings = request.json
    db.session.commit()
    return jsonify({'status': 'updated'})

# SAFE: Proper authorization for privilege operations
@app.route('/api/user/<user_id>/promote', methods=['POST'])
@login_required
@admin_required
def promote_user(user_id):
    # Only admins can reach this (decorator)
    user = User.query.get_or_404(user_id)

    # Additional business logic checks
    if user.role == 'admin':
        abort(400, 'User is already an admin')

    user.role = 'admin'
    db.session.commit()
    audit_log.info(f'User {user_id} promoted by {current_user.id}')
    return jsonify({'status': 'promoted'})
// SAFE: Path validation to prevent traversal
@GetMapping("/files/{filename}")
public ResponseEntity<Resource> getFile(@PathVariable String filename) {
    // Validate filename
    if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
        return ResponseEntity.badRequest().build();
    }

    Path basePath = Paths.get("/uploads").toAbsolutePath().normalize();
    Path filePath = basePath.resolve(filename).normalize();

    // Ensure resolved path is within base directory
    if (!filePath.startsWith(basePath)) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
    }

    Resource resource = new FileSystemResource(filePath);
    if (!resource.exists()) {
        return ResponseEntity.notFound().build();
    }

    return ResponseEntity.ok(resource);
}

// SAFE: Use authenticated user's account
@PostMapping("/transfer")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> transfer(
        @RequestBody TransferRequest request,
        @AuthenticationPrincipal UserDetails user) {

    // Get account from authenticated user, not request
    Account fromAccount = accountRepository.findByUserId(user.getId());

    // Verify user owns the source account
    if (fromAccount == null) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
    }

    accountService.transfer(
        fromAccount.getId(),
        request.getToAccount(),
        request.getAmount()
    );

    return ResponseEntity.ok().build();
}

// SAFE: Function-level access control
@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> deleteUser(@PathVariable Long id, Principal principal) {
    // Verify not deleting self
    if (id.equals(getUserId(principal))) {
        return ResponseEntity.badRequest()
            .body("Cannot delete your own account");
    }

    userRepository.deleteById(id);
    auditService.log("USER_DELETED", id, principal.getName());

    return ResponseEntity.ok().build();
}
// SAFE: Server-side access control
const isAdmin = (req, res, next) => {
    if (!req.user || !req.user.roles.includes('admin')) {
        return res.status(403).json({ error: 'Admin access required' });
    }
    next();
};

app.get('/api/admin/config', authenticate, isAdmin, (req, res) => {
    res.json(systemConfig);
});

// SAFE: Ownership verification with UUIDs
app.get('/api/documents/:id', authenticate, async (req, res) => {
    const doc = await Document.findById(req.params.id);

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

    // Check ownership or sharing
    if (doc.ownerId !== req.user.id &&
        !doc.sharedWith.includes(req.user.id) &&
        !req.user.roles.includes('admin')) {
        return res.status(403).json({ error: 'Access denied' });
    }

    res.json(doc);
});

// SAFE: Whitelist allowed fields
app.put('/api/profile', authenticate, async (req, res) => {
    // Only allow specific fields to be updated
    const allowedFields = ['name', 'email', 'preferences'];
    const updates = {};

    for (const field of allowedFields) {
        if (req.body[field] !== undefined) {
            updates[field] = req.body[field];
        }
    }

    await User.findByIdAndUpdate(req.user.id, updates);
    res.json({ success: true });
});

Exploited in the Wild

Facebook Privacy Bug (2018)

A privacy bug allowed apps to access photos that users uploaded but never shared, affecting 6.8 million users. The access control flaw exposed private photos to third-party applications.

Uber "God View" (2014)

Uber employees had access to a "God View" tool that tracked customer locations in real-time with insufficient access controls, leading to privacy violations and regulatory action.

First American Financial (2019)

885 million customer records were exposed due to an IDOR vulnerability where documents could be accessed by incrementing document IDs in URLs.


Tools to test/exploit

  • Burp Suite — comprehensive access control testing.

  • OWASP ZAP — automated security testing.

  • Autorize — Burp extension for access control testing.

  • Postman — API testing for access control verification.


CVE Examples


References

  1. MITRE. "CWE-284: Improper Access Control." https://cwe.mitre.org/data/definitions/284.html

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