Authorization Bypass Through User-Controlled Key

Description

Authorization Bypass Through User-Controlled Key occurs when the system's authorization functionality does not prevent one user from accessing another user's data by modifying the key value identifying the data. This is commonly known as Insecure Direct Object Reference (IDOR). The vulnerability arises when applications use user-supplied input (such as IDs, filenames, or keys) to directly access objects without verifying the user's authorization to access that specific object. Attackers simply modify these references to access unauthorized resources.

Risk

IDOR vulnerabilities are extremely common and often critical. They can expose sensitive data of other users, enable unauthorized modifications, and lead to complete data breaches. Because exploitation is trivial (just changing an ID), automated tools can quickly enumerate and extract large amounts of data. Notable breaches affecting millions of users have resulted from IDOR vulnerabilities. The First American Financial exposure of 885 million records was caused by sequential document IDs without authorization checks.

Solution

Always verify that the authenticated user has permission to access the requested resource. Implement object-level authorization checks. Use indirect references that map to actual objects server-side. Consider using UUIDs instead of sequential IDs to reduce predictability (but still implement authorization). Implement proper access control lists. Log and monitor access patterns for anomalies. Test authorization on every access path to sensitive resources.

Common Consequences

ImpactDetails
ConfidentialityScope: Data Exposure

Unauthorized access to other users' personal data, documents, and records.
IntegrityScope: Data Modification

Ability to modify or delete other users' data by manipulating object references.
PrivacyScope: Privacy Violation

Access to private communications, medical records, financial data of other users.

Example Code + Solution Code

Vulnerable Code

# VULNERABLE: Direct object reference without authorization
@app.route('/api/users/<user_id>/profile')
@login_required
def get_profile(user_id):
    # Any authenticated user can access any profile!
    user = User.query.get(user_id)
    return jsonify(user.to_dict())

# VULNERABLE: Document access by ID
@app.route('/api/documents/<doc_id>')
@login_required
def get_document(doc_id):
    # Just incrementing doc_id exposes all documents
    document = Document.query.get(doc_id)
    return send_file(document.path)

# VULNERABLE: Order details
@app.route('/api/orders/<order_id>')
@login_required
def get_order(order_id):
    # Attacker changes order_id to view other users' orders
    order = Order.query.get(order_id)
    return jsonify({
        'items': order.items,
        'total': order.total,
        'shipping_address': order.shipping_address,  # PII exposed!
        'payment_method': order.payment_method
    })

# VULNERABLE: File download
@app.route('/download')
@login_required
def download_file():
    filename = request.args.get('file')
    # Attacker: ?file=../../../etc/passwd or ?file=user_123_data.pdf
    return send_from_directory('/uploads', filename)
// VULNERABLE: REST endpoint with IDOR
@RestController
@RequestMapping("/api")
public class UserController {

    @GetMapping("/users/{userId}/messages")
    public List<Message> getMessages(@PathVariable Long userId) {
        // No check if authenticated user is userId!
        return messageRepository.findByUserId(userId);
    }

    @DeleteMapping("/invoices/{invoiceId}")
    public ResponseEntity<?> deleteInvoice(@PathVariable Long invoiceId) {
        // Any user can delete any invoice
        invoiceRepository.deleteById(invoiceId);
        return ResponseEntity.ok().build();
    }

    @PutMapping("/accounts/{accountId}")
    public ResponseEntity<?> updateAccount(
            @PathVariable Long accountId,
            @RequestBody AccountUpdate update) {
        // Attacker modifies any account
        Account account = accountRepository.findById(accountId).orElseThrow();
        account.setEmail(update.getEmail());
        accountRepository.save(account);
        return ResponseEntity.ok().build();
    }
}
// VULNERABLE: Express API with IDOR
app.get('/api/users/:userId/data', authenticate, async (req, res) => {
    // userId from URL, not validated against authenticated user
    const data = await UserData.findOne({ userId: req.params.userId });
    res.json(data);  // Returns any user's data
});

// VULNERABLE: GraphQL query
const resolvers = {
    Query: {
        user: (_, { id }) => {
            // No authorization check
            return User.findById(id);
        },
        invoice: (_, { id }) => {
            // Anyone can query any invoice
            return Invoice.findById(id);
        }
    }
};

// VULNERABLE: File serving
app.get('/files/:fileId', authenticate, async (req, res) => {
    const file = await File.findById(req.params.fileId);
    // No ownership check
    res.sendFile(file.path);
});

Fixed Code

# SAFE: Verify ownership before access
@app.route('/api/users/<user_id>/profile')
@login_required
def get_profile(user_id):
    # Verify user is accessing their own profile or is admin
    if str(current_user.id) != user_id and not current_user.is_admin:
        abort(403, 'Access denied')

    user = User.query.get_or_404(user_id)
    return jsonify(user.to_dict())

# SAFE: Check document ownership
@app.route('/api/documents/<doc_id>')
@login_required
def get_document(doc_id):
    document = Document.query.get_or_404(doc_id)

    # Verify ownership or sharing permission
    if not document.can_access(current_user):
        abort(403, 'Access denied')

    return send_file(document.path)

# SAFE: Filter orders by authenticated user
@app.route('/api/orders/<order_id>')
@login_required
def get_order(order_id):
    # Query includes user constraint
    order = Order.query.filter_by(
        id=order_id,
        user_id=current_user.id
    ).first_or_404()

    return jsonify(order.to_dict())

# SAFE: Use indirect references
ALLOWED_DOWNLOADS = {
    'report': 'monthly_report.pdf',
    'invoice': 'invoice_template.pdf',
    'guide': 'user_guide.pdf'
}

@app.route('/download/<file_key>')
@login_required
def download_file(file_key):
    filename = ALLOWED_DOWNLOADS.get(file_key)
    if not filename:
        abort(404)

    # User-specific files
    user_path = f'/uploads/{current_user.id}'
    return send_from_directory(user_path, filename)

# SAFE: Object-level authorization decorator
def authorize_resource(model, id_param='id', owner_field='user_id'):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            resource_id = kwargs.get(id_param)
            resource = model.query.get_or_404(resource_id)

            if getattr(resource, owner_field) != current_user.id:
                if not current_user.is_admin:
                    abort(403)

            kwargs['resource'] = resource
            return f(*args, **kwargs)
        return wrapper
    return decorator

@app.route('/api/posts/<post_id>')
@login_required
@authorize_resource(Post, 'post_id', 'author_id')
def get_post(post_id, resource):
    return jsonify(resource.to_dict())
// SAFE: Service layer authorization
@Service
public class SecureUserService {

    @Autowired
    private MessageRepository messageRepository;

    public List<Message> getMessages(Long userId, Authentication auth) {
        User currentUser = (User) auth.getPrincipal();

        // Verify authorization
        if (!currentUser.getId().equals(userId) &&
            !currentUser.hasRole("ADMIN")) {
            throw new AccessDeniedException("Cannot access other user's messages");
        }

        return messageRepository.findByUserId(userId);
    }
}

@RestController
@RequestMapping("/api")
public class SecureController {

    @Autowired
    private SecureUserService userService;

    @GetMapping("/users/{userId}/messages")
    public List<Message> getMessages(
            @PathVariable Long userId,
            Authentication auth) {
        return userService.getMessages(userId, auth);
    }

    @DeleteMapping("/invoices/{invoiceId}")
    @PreAuthorize("@invoiceSecurityService.canDelete(#invoiceId, authentication)")
    public ResponseEntity<?> deleteInvoice(@PathVariable Long invoiceId) {
        invoiceRepository.deleteById(invoiceId);
        return ResponseEntity.ok().build();
    }
}

@Service
public class InvoiceSecurityService {

    @Autowired
    private InvoiceRepository invoiceRepository;

    public boolean canDelete(Long invoiceId, Authentication auth) {
        User user = (User) auth.getPrincipal();
        Invoice invoice = invoiceRepository.findById(invoiceId).orElse(null);

        if (invoice == null) return false;

        // Owner or admin can delete
        return invoice.getUserId().equals(user.getId()) ||
               user.hasRole("ADMIN");
    }
}
// SAFE: Authorization middleware
const authorizeResource = (model, ownerField = 'userId') => {
    return async (req, res, next) => {
        const resourceId = req.params.id || req.params[`${model.modelName.toLowerCase()}Id`];

        const resource = await model.findById(resourceId);
        if (!resource) {
            return res.status(404).json({ error: 'Not found' });
        }

        // Check ownership
        if (resource[ownerField].toString() !== req.user.id &&
            !req.user.roles.includes('admin')) {
            return res.status(403).json({ error: 'Access denied' });
        }

        req.resource = resource;
        next();
    };
};

// Usage
app.get('/api/users/:id/data',
    authenticate,
    authorizeResource(UserData),
    async (req, res) => {
        res.json(req.resource);
    }
);

// SAFE: GraphQL with authorization
const resolvers = {
    Query: {
        user: async (_, { id }, context) => {
            if (!context.user) {
                throw new AuthenticationError('Not authenticated');
            }

            // Can only query own user or if admin
            if (context.user.id !== id && !context.user.isAdmin) {
                throw new ForbiddenError('Access denied');
            }

            return User.findById(id);
        },

        invoice: async (_, { id }, context) => {
            const invoice = await Invoice.findById(id);

            if (!invoice) {
                throw new NotFoundError('Invoice not found');
            }

            if (invoice.userId !== context.user.id && !context.user.isAdmin) {
                throw new ForbiddenError('Access denied');
            }

            return invoice;
        }
    }
};

// SAFE: File access with ownership
app.get('/files/:fileId', authenticate, async (req, res) => {
    const file = await File.findById(req.params.fileId);

    if (!file) {
        return res.status(404).json({ error: 'File not found' });
    }

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

    res.sendFile(file.path);
});

Exploited in the Wild

First American Financial (2019)

885 million documents containing sensitive personal and financial data were exposed due to sequential document IDs without authorization checks. Anyone could access any document by incrementing the ID.

Parler Data Scrape (2021)

Parler's API used sequential post IDs without authorization, allowing researchers to download the entire platform's content including deleted posts and user data.

Facebook Photo Exposure (2018)

A bug allowed apps to access photos that users uploaded but never shared, affecting 6.8 million users, due to improper authorization on photo IDs.


Tools to test/exploit

  • Burp Suite — intercept and modify ID parameters.

  • Autorize — Burp extension for IDOR testing.

  • OWASP ZAP — automated IDOR detection.

  • ffuf — ID enumeration and fuzzing.


CVE Examples


References

  1. MITRE. "CWE-639: Authorization Bypass Through User-Controlled Key." https://cwe.mitre.org/data/definitions/639.html

  2. OWASP. "Insecure Direct Object References." https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/05-Authorization_Testing/04-Testing_for_Insecure_Direct_Object_References