Bola Idor in Nestjs
How Bola Idor Manifests in Nestjs
Broken Object Level Authorization (BOLA), also known as Insecure Direct Object Reference (IDOR), occurs when an application fails to properly validate whether a user has permission to access a specific object. In NestJS applications, this vulnerability often manifests through predictable patterns that developers might overlook.
The most common BOLA pattern in NestJS involves directly using request parameters to fetch database records without validating ownership. Consider a typical user profile endpoint:
@Get('users/:id')
@UseGuards(AuthGuard('jwt'))
async getUser(@Param('id') id: string) {
return await this.userService.findById(id);
}
The critical flaw here is that the endpoint trusts the id parameter from the URL. Any authenticated user can request /users/1, /users/2, etc., and retrieve other users' data. The @UseGuards(AuthGuard('jwt')) only verifies authentication, not authorization.
Another NestJS-specific manifestation occurs with entity relations. When using @Relation decorators or TypeORM relations:
@Get('posts/:postId/comments')
async getPostComments(@Param('postId') postId: string) {
return await this.commentService.findByPostId(postId);
}
Similar to the user example, this allows any authenticated user to view comments on any post. The service layer might look like:
async findByPostId(postId: string) {
return await this.commentRepository.find({ where: { postId } });
}
Without checking whether the requesting user owns or has permission to view the post, this is a classic BOLA vulnerability.
NestJS applications often use DTOs and validation pipes, which can create a false sense of security. Developers might assume that because the id parameter is validated as a number or UUID, it's safe to use directly. However, validation only ensures format correctness, not authorization.
Batch operations in NestJS present another BOLA vector. When endpoints accept arrays of IDs:
@Post('bulk-delete')
@UseGuards(AuthGuard('jwt'))
async bulkDelete(@Body() dto: BulkDeleteDto) {
return await this.itemService.deleteMany(dto.ids);
}
Without verifying that the user owns all items in dto.ids, an attacker can delete resources belonging to other users.
NestJS-Specific Detection
Detecting BOLA in NestJS applications requires both manual code review and automated scanning. The middleBrick API security scanner excels at identifying these vulnerabilities in NestJS applications through black-box testing.
middleBrick's approach to NestJS BOLA detection includes:
- Parameter manipulation testing - The scanner systematically modifies object IDs in API requests to check if unauthorized access is possible
- Authentication state testing - It tests endpoints with different authenticated user contexts to verify proper authorization boundaries
- Response analysis - middleBrick examines API responses for data leakage patterns that indicate BOLA vulnerabilities
- Schema-aware testing - When provided with OpenAPI specs, middleBrick cross-references parameter definitions with actual runtime behavior
For NestJS developers, the middleBrick CLI provides immediate feedback:
npx middlebrick scan https://api.yourapp.com/users/1
This command performs a 5-15 second scan, testing the unauthenticated attack surface and providing a security score with specific BOLA findings.
Manual detection in NestJS code involves searching for patterns like:
// Search for these patterns:
@Get(':id')
@Get('users/:userId')
@Get('posts/:postId')
@Param('id')
@Param('userId')
@Param('postId')
Once identified, examine the service layer for authorization checks. A vulnerable service might look like:
async findById(id: string) {
return await this.repo.findOne(id); // No ownership check!
}
middleBrick's OpenAPI analysis is particularly valuable for NestJS applications, as it can detect BOLA vulnerabilities even in complex nested routes and parameter combinations that manual review might miss.
NestJS-Specific Remediation
Remediating BOLA vulnerabilities in NestJS requires implementing proper authorization checks at the controller or service layer. NestJS provides several patterns for secure authorization.
The most straightforward approach uses custom guards:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class OwnershipGuard implements CanActivate {
constructor(private reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise {
const request = context.switchToHttp().getRequest();
const user = request.user;
const id = request.params.id;
// Verify ownership
const isOwner = await this.verifyOwnership(user.id, id);
return isOwner;
}
private async verifyOwnership(userId: string, objectId: string): Promise {
// Implement your ownership logic
return await this.repo.exists({ id: objectId, ownerId: userId });
}
}
Apply this guard to vulnerable endpoints:
@Get('users/:id')
@UseGuards(AuthGuard('jwt'), OwnershipGuard)
async getUser(@Param('id') id: string) {
return await this.userService.findById(id);
}
For more complex authorization scenarios, NestJS's RolesGuard can be extended:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class ResourceGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise {
const request = context.switchToHttp().getRequest();
const user = request.user;
const resourceId = request.params.id;
// Check if user has access to this resource
return await this.resourceService.userHasAccess(user.id, resourceId);
}
}
NestJS's dependency injection makes it easy to centralize authorization logic in services:
@Injectable()
export class UserService {
async findById(id: string, requestingUserId: string) {
const user = await this.repo.findOne({
where: { id },
// Ensure requesting user has permission
relations: ['permissions']
});
if (!user || !await this.hasPermission(requestingUserId, user)) {
throw new UnauthorizedException();
}
return user;
}
}
The controller then passes the authenticated user ID:
@Get('users/:id')
@UseGuards(AuthGuard('jwt'))
async getUser(@Param('id') id: string, @Request() req) {
return await this.userService.findById(id, req.user.id);
}
For batch operations, implement ownership verification for all items:
async bulkDelete(ids: string[], userId: string) {
const ownedItems = await this.repo.find({
where: { id: In(ids), ownerId: userId }
});
if (ownedItems.length !== ids.length) {
throw new BadRequestException('Cannot delete items you don''t own');
}
return await this.repo.remove(ownedItems);
}
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |