HIGH broken access controlnestjsapi keys

Broken Access Control in Nestjs with Api Keys

Broken Access Control in Nestjs with Api Keys — how this specific combination creates or exposes the vulnerability

Broken Access Control occurs when API endpoints do not properly enforce authorization checks, allowing one user to access or modify another user’s resources. In NestJS applications that rely on API keys for authentication, this risk arises when keys are treated as secrets that grant access but are not coupled with role-based or ownership-based authorization checks at the route or handler level.

API keys are often implemented as simple strings passed in headers, query parameters, or cookies. If these keys identify a tenant or a user but the downstream controller or service does not verify that the requesting entity is allowed to perform the action on the targeted resource, the API becomes vulnerable to BOLA (Broken Object Level Authorization) and IDOR (Insecure Direct Object References). For example, an endpoint like GET /users/:userId/profile that only validates the presence of a valid API key but does not ensure the key’s associated subject matches :userId can allow horizontal privilege escalation.

NestJS does not automatically enforce authorization based on authentication metadata. If developers implement API key validation in a guard or an interceptor but omit per-request ownership checks, the unauthenticated attack surface includes endpoints where object-level permissions should apply. Attackers can probe predictable numeric IDs or UUIDs and read or modify data across tenant boundaries. The risk is compounded when OpenAPI specs are not rigorously aligned with runtime behavior: a spec may indicate scoped access, but missing runtime checks expose broader surfaces.

Another vector involves role or scope validation. API keys may embed scopes (e.g., read:posts, write:posts), but if NestJS controllers rely solely on the presence of a key and do not inspect scope claims or roles, endpoints that should be restricted to privileged roles become accessible to any key with minimal privileges. This is a classic Broken Access Control pattern where the authorization model is weakly enforced.

Middleware or guards that only check for key validity, without correlating the key to a user context and the requested resource, create a gap between authentication and authorization. In black-box scans, such misconfigurations appear as endpoints that return 200 for unauthorized subjects when they should return 403 or 404. The combination of NestJS’s flexible guard system and the simplicity of API keys can inadvertently expose data or operations if authorization logic is not consistently applied across all routes and methods.

Api Keys-Specific Remediation in Nestjs — concrete code fixes

Remediation centers on ensuring that API key validation is followed by explicit authorization checks that tie the key to the correct subject and enforce scope or role requirements. Below are concrete, syntactically correct examples that demonstrate how to implement this in NestJS.

1. API key validation with ownership check

Use a guard that extracts the API key, resolves the associated user, and then verifies ownership of the requested resource. This ensures that even if a key is valid, the caller can only access their own data.

@Injectable()
export class UserOwnershipGuard implements CanActivate {
  constructor(private readonly usersService: UsersService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const apiKey = request.headers['x-api-key'];
    const userId = request.params.userId;

    if (!apiKey) {
      throw new UnauthorizedException('API key missing');
    }

    const keyRecord = await this.usersService.validateApiKey(apiKey);
    if (!keyRecord || !keyRecord.active) {
      throw new UnauthorizedException('Invalid API key');
    }

    // Ownership check: ensure the key belongs to the user being accessed
    if (keyRecord.userId !== userId) {
      throw new ForbiddenException('Access denied: insufficient permissions');
    }

    request.user = keyRecord.userId;
    return true;
  }
}

Apply this guard on routes that access user-specific resources:

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':userId/profile')
  @UseGuards(AuthApiKey, UserOwnershipGuard)
  async getProfile(@Param('userId') userId: string, @Request() req: Request) {
    // req.user is guaranteed to match userId due to UserOwnershipGuard
    return this.usersService.getProfile(userId);
  }
}

2. Scope-based authorization with API keys

Store scopes in the key metadata and enforce them at the controller or handler level. This prevents privilege escalation when keys have broader authentication scope than intended.

@Injectable()
export class ScopeAuthorizationGuard implements CanActivate {
  constructor() {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const requiredScope = this.reflectRequiredScope(context);
    const apiKey = request.headers['x-api-key'];

    if (!apiKey) {
      throw new UnauthorizedException('API key missing');
    }

    const keyRecord = await this.validateKeyWithScope(apiKey);
    if (!keyRecord || !this.hasScope(keyRecord.scopes, requiredScope)) {
      throw new ForbiddenException('Insufficient scope');
    }
    return true;
  }

  private reflectRequiredScope(context: ExecutionContext): string {
    const handler = context.getHandler();
    const required = Reflect.getMetadata('scope', handler);
    if (!required) {
      throw new ForbiddenException('Scope metadata not defined');
    }
    return required;
  }

  private hasScope(keyScopes: string[], required: string): boolean {
    return keyScopes.includes(required);
  }

  private async validateKeyWithScope(apiKey: string): Promise<KeyRecord | null> {
    // Example: fetch key with scopes from a data source
    return { key: apiKey, scopes: ['read:posts', 'write:comments'] } as KeyRecord;
  }
}

interface KeyRecord {
  key: string;
  scopes: string[];
}

Use metadata to declare required scopes on handlers:

@Controller('posts')
export class PostsController {
  @Get()
  @UseGuards(AuthApiKey, ScopeAuthorizationGuard)
  @Scope('read:posts')
  async findAll() {
    return ['post1', 'post2'];
  }

  @Post()
  @UseGuards(AuthApiKey, ScopeAuthorizationGuard)
  @Scope('write:posts')
  async create() {
    return { ok: true };
  }
}

3. Middleware for global key validation with per-route mapping

Use middleware to normalize key validation, but always enforce granular authorization in guards or interceptors. Do not rely on middleware alone for per-endpoint permissions.

@Injectable()
export class ApiKeyMiddleware implements NestMiddleware {
  constructor(private readonly keysService: ApiKeysService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const key = req.headers['x-api-key'] || req.query.key;
    if (!key) {
      res.status(401).json({ error: 'API key required' });
      return;
    }
    const isValid = await this.keysService.checkKey(key);
    if (!isValid) {
      res.status(403).json({ error: 'Invalid API key' });
      return;
    }
    // Attach key metadata for downstream guards
    req.apiKey = key;
    next();
  }
}

Apply the middleware selectively and combine with guards for precise control:

@Middleware(ApiKeyMiddleware)
@Controller('items')
export class ItemsController {
  constructor(private itemsService: ItemsService) {}

  @Get(':itemId')
  @UseGuards(OwnershipOrScopeGuard)
  async getItem(@Param('itemId') itemId: string, @Request() req: Request) {
    return this.itemsService.getItem(itemId, req.apiKey);
  }
}

These patterns ensure that API keys authenticate requests while explicit authorization checks enforce object-level and scope-based access control, mitigating Broken Access Control in NestJS applications.

Frequently Asked Questions

How does middleBrick detect Broken Access Control in NestJS API key setups?
middleBrick runs black-box scans without credentials, testing unauthenticated and low-privilege surfaces. It checks whether endpoints that should be restricted allow mismatched IDs or missing scope checks, and maps findings to OWASP API Top 10 and compliance frameworks.
Can middleBrick fix the vulnerabilities it finds in NestJS APIs?
middleBrick detects and reports findings with remediation guidance; it does not fix, patch, block, or remediate. Developers should apply code-level fixes such as ownership and scope checks as demonstrated in the remediation examples.