Broken Object Level Authorization

Broken Object Level Authorization in NestJS [Mar 2026] [CVE-2026-2293]

[Fixed Mar 2026] Updated CVE-2026-2293

Overview

CVE-2026-2293 describes a broken object-level authorization scenario in NestJS when using @nestjs/platform-fastify with Fastify path-normalization options enabled. In such configurations, authentication or authorization middleware can be bypassed, allowing an attacker to access or manipulate resources they should not be entitled to. The vulnerability is classified under CWE-863 (Incorrect Authorization). It manifested in a NestJS 11.1.x release (listed as affected: 11.1.13) where path normalization could cause requests to be processed in a way that bypassed route-guarded checks, effectively enabling access to protected endpoints by manipulating the request path. This is a critical BOLA pattern because authorization decisions were made on altered or ambiguous paths rather than on robust, resource-level checks. In real-world applications, attackers could enumerate or access user-owned resources (or admin endpoints) without proper ownership validation if the app relies on path-based routing behavior that can be bypassed by normalization, rather than explicit, per-resource authorization logic. The remediation centers on removing reliance on path normalization for security, boosting explicit resource-level authorization, and ensuring authenticated access is consistently enforced across all routes. The guide here translates that into concrete NestJS (TypeScript) fixes and safe configuration patterns.

Affected Versions

11.1.13

Code Fix Example

NestJS API Security Remediation
// Vulnerable pattern and the fix side-by-side for NestJS with Fastify path normalization enabled

// Vulnerable setup (path normalization enabled) and naive authorization check
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { HttpException, Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

async function bootstrapVulnerable() {
  // Enabled path normalization (example: ignoreTrailingSlash/caseSensitive) which can affect route matching before guards run
  const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter({ ignoreTrailingSlash: true, caseSensitive: false }));
  // Global middleware or guard that relies on path to decide authorization (flawed pattern)
  app.useGlobalGuards(PathBasedAuthGuard);
  await app.listen(3000);
}

@Injectable()
class PathBasedAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    // Flawed: uses req.path to authorize/admin access; path normalization can bypass this check
    if (req.path.startsWith('/admin')) {
      // Only checks presence of a user; does not verify ownership/permissions
      return !!req.user;
    }
    return true;
  }
}

bootstrapVulnerable();

// Fixed version: enforce robust, resource-level authorization and disable path normalization effects
async function bootstrapFixed() {
  // Disable path normalization side effects by choosing stricter Fastify options
  const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter({ ignoreTrailingSlash: false, caseSensitive: true }));
  // Apply a proper OwnershipGuard at route level or globally that checks actual resource ownership
  app.useGlobalGuards(OwnershipGuard);
  await app.listen(3000);
}

@Injectable()
class OwnershipGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req: any = context.switchToHttp().getRequest();
    const userId = req.user?.id;
    const resourceId = req.params?.id;
    // Realistic ownership check via a service call (pseudo-implementation here)
    const owns = await userOwnsResource(userId, resourceId);
    if (!owns) {
      throw new HttpException('Forbidden', 403);
    }
    return true;
  }
}

async function userOwnsResource(userId: string, resourceId: string): Promise<boolean> {
  // Replace with actual service call to verify ownership, e.g., await resourcesService.isOwner(userId, resourceId)
  return !!userId && !!resourceId;
}

bootstrapFixed();

CVE References

Choose which optional cookies to allow. You can change this any time.