Bola Idor in Nestjs with Cockroachdb
Bola Idor in Nestjs with Cockroachdb — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) occurs when an API exposes one user’s data or operations by manipulating object identifiers. In a NestJS application using CockroachDB, BOLA commonly arises when route parameters such as :id are bound directly to database queries without ensuring the requesting user is authorized for that specific resource.
CockroachDB is PostgreSQL-wire compatible, so the typical NestJS stack uses an ORM like TypeORM with a CockroachDB driver. If an endpoint like GET /users/:id constructs SQL such as SELECT * FROM users WHERE id = $1 using only the route parameter, an attacker can change :id to another valid UUID and read or act on other users’ records. The risk is not CockroachDB itself, but the lack of tenant or ownership checks in application logic combined with an ORM that may expose raw identifiers.
Common patterns that enable BOLA include:
- Using raw query parameters without scoping to a user or tenant.
- Exposing internal identifiers (e.g., sequential or easily guessable IDs) in URLs.
- Assuming the ORM layer enforces ownership when it does not.
In a microservices or serverless deployment where CockroachDB serves many tenants, BOLA can become lateral movement across customer boundaries. For example, an attacker might iterate over valid-looking IDs and observe differences in response size or timing, or exploit missing authorization checks in related endpoints such as PUT /accounts/:accountId or DELETE /organizations/:orgId. OWASP API Top 10 A1: Broken Object Level Authorization maps directly to this class of risk.
Key contributing factors in the NestJS + CockroachDB context include:
- Route parameters and query parameters that are not validated against the requesting user’s scope.
- JWT or session claims that are not enforced at the data-access layer.
- Endpoints that expose relationships (e.g.,
/users/:id/orders) without confirming that the user owns the related parent resource.
Cockroachdb-Specific Remediation in Nestjs — concrete code fixes
Remediation focuses on scoping every data access to the requesting user or tenant and enforcing authorization before any database operation. Below are concrete, production-grade patterns for NestJS with CockroachDB via TypeORM.
1. Always scope queries by authenticated subject
Ensure every repository or service method includes the user identifier derived from the request context (e.g., from JWT). Never rely on the caller to provide an identifier that maps 1:1 to object ownership without verification.
// user.entity.ts
import { Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
// other fields
}
// users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepo: Repository,
) {}
async findOneByIdentity(userId: string, requestingUserId: string): Promise {
const user = await this.usersRepo.findOne({
where: { id: userId, tenantId: requestingUserId }, // or a join/ownership check
});
if (!user) {
throw new NotFoundException('User not found or access denied');
}
return user;
}
}
2. Use a base repository or decorator to enforce tenant/ownership scoping
Centralize scoping logic to avoid accidental bypasses. With CockroachDB, you can add a tenant column or use row-level security (RLS) if the table is user-specific. Here we show a simple scoped repository helper.
// scoped-users.repository.ts
import { Repository } from 'typeorm';
import { User } from './user.entity';
export class ScopedUserRepository extends Repository {
async findScopedById(id: string, tenantId: string): Promise {
return this.createQueryBuilder('user')
.where('user.id = :id', { id })
.andWhere('user.tenantId = :tenantId', { tenantId })
.getOne();
}
}
// users.controller.ts
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { ScopedUserRepository } from './scoped-users.repository';
import { AuthGuard } from '@nestjs/passport';
@Controller('users')
export class UsersController {
constructor(private scopedUsers: ScopedUserRepository) {}
@UseGuards(AuthGuard('jwt'))
@Get(':id')
async getUser(@Param('id') id: string, @Req() req: any) {
const user = await this.scopedUsers.findScopedById(id, req.user.tenantId);
if (!user) {
throw new NotFoundException('Forbidden or not found');
}
return user;
}
}
3. Validate and canonicalize identifiers
Treat incoming IDs as untrusted. Reject malformed IDs early and avoid exposing raw database keys when feasible. Use UUID validation and ensure case-sensitivity is consistent to prevent bypass via casing tricks.
// id.validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { v4 as uuidRegex } from 'uuid';
@Injectable()
export class IdValidationPipe implements PipeTransform {
transform(value: string, metadata: ArgumentMetadata) {
if (!uuidRegex().test(value)) {
throw new BadRequestException('Invalid resource identifier');
}
return value;
}
}
// usage in route
@Get(':id')
async getUser(@Param('id', new IdValidationPipe()) id: string, @Req() req: any) {
// proceed with scoped query
}
4. Avoid exposing sequential or guessable identifiers
If feasible, prefer opaque identifiers (e.g., UUIDv4) stored in a dedicated, indexed column. CockroachDB handles UUID well, and using non-sequential IDs prevents enumeration attacks that complement BOLA.
5. Leverage application-level joins and ownership checks
For relationships (e.g., user → orders), always join on tenant/owner columns rather than trusting foreign keys alone. Example with TypeORM and CockroachDB:
// orders.service.ts
async getOrdersForUser(userId: string, requestingUserId: string) {
return this.ordersRepo.createQueryBuilder('order')
.leftJoinAndSelect('order.user', 'user')
.where('order.userId = :userId', { userId })
.andWhere('user.tenantId = :tenantId', { tenantId: requestingUserId })
.getMany();
}
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 |