← All posts
September 6, 2026 · Sécurité API

REST & GraphQL API Auditing: Eliminating BOLA/IDOR Vulnerabilities in Node.js & Next.js

BOLA (Broken Object Level Authorization) causes over 80% of API data breaches. Learn how to audit, detect, and remediate object-level authorization vulnerabilities with strict access controls.

Introduction & Context

In the OWASP API Security Top 10, API1:2023 - Broken Object Level Authorization (BOLA) — historically known as IDOR (Insecure Direct Object Reference) — holds the #1 spot.

It is the single most devastating security vulnerability in modern web applications, SaaS platforms, and REST/GraphQL APIs. BOLA occurs when an API endpoint accepts an object identifier (e.g., /api/documents/8492 or a GraphQL mutation updateUser(id: "8492")) without verifying whether the authenticated user has legitimate ownership or access permissions over that specific object.


1. Why Authentication (JWT/OAuth) Does NOT Protect Against BOLA

A common misconception among developers is assuming that a valid authentication middleware (e.g., a valid JWT returning HTTP 200) automatically secures an API endpoint.

┌─────────────────────────┐
│ HTTP Request (Bearer JWT)│ ──> [ Auth OK ] (User is 'Bob')
└─────────────────────────┘
                                           │
                                           ▼
┌─────────────────────────┐
│ Request: /api/orders/99 │ ──> [ No BOLA check! ] ──> Returns Alice's order!
└─────────────────────────┘
  • Authentication: Answers "Who is this user?" (e.g., Bob).
  • Object-Level Authorization (BOLA): Answers "Does Bob have the right to read/modify object #99 which belongs to Alice?".

2. Concrete Examples & Vulnerable Code

❌ Example 1: Next.js / Node.js REST Route Handler with Classic BOLA Vulnerability

import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { verifyJwt } from "@/lib/auth";

export async function GET(request: Request, { params }: { params: { invoiceId: string } }) {
  const authHeader = request.headers.get("authorization");
  const user = verifyJwt(authHeader);

  // ❌ User is authenticated...
  if (!user) {
    return NextResponse.json({ error: "Unauthenticated" }, { status: 401 });
  }

  // ❌ CRITICAL BOLA VULNERABILITY: Fetching invoice ONLY by its ID!
  // If Bob enters Alice's invoice ID (invoiceId = "inv_9999"), the server returns it!
  const invoice = await db.invoice.findUnique({
    where: { id: params.invoiceId }
  });

  if (!invoice) {
    return NextResponse.json({ error: "Invoice not found" }, { status: 404 });
  }

  return NextResponse.json(invoice);
}
  • Attack Scenario: An attacker signs up, gets their own invoice inv_0001, and runs a script iterating over inv_0002, inv_0003... stealing financial data across all accounts in seconds.

❌ Example 2: GraphQL Mutation Vulnerable to BOLA

# Exposed GraphQL Mutation
mutation DeleteUserDocument($documentId: ID!) {
  deleteDocument(id: $documentId) {
    success
  }
}
// Vulnerable GraphQL Resolver
const resolvers = {
  Mutation: {
    deleteDocument: async (_, { id }, context) => {
      if (!context.user) throw new Error("Unauthenticated");

      // ❌ BOLA VULNERABILITY: Deleting directly by ID without verifying context.user.id
      await db.document.delete({ where: { id } });
      return { success: true };
    }
  }
};

3. Complete Remediation & Secure Code

To permanently eliminate BOLA vulnerabilities, enforce this golden rule: Every database read, update, or delete operation MUST include the active session's userId or organizationId in the query condition.

✅ Corrected REST Route (Strict Object-Level Authorization)

import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { verifyJwt } from "@/lib/auth";

export async function GET(request: Request, { params }: { params: { invoiceId: string } }) {
  const authHeader = request.headers.get("authorization");
  const user = verifyJwt(authHeader);

  if (!user) {
    return NextResponse.json({ error: "Unauthenticated" }, { status: 401 });
  }

  // ✅ BOLA FIX: Query condition combines resource ID AND owner ID
  const invoice = await db.invoice.findFirst({
    where: {
      id: params.invoiceId,
      userId: user.id, // 🔒 Only invoices owned by the authenticated user will be returned
    }
  });

  if (!invoice) {
    // Returning 404 instead of 403 prevents revealing the existence of other users' resources
    return NextResponse.json({ error: "Invoice not found" }, { status: 404 });
  }

  return NextResponse.json(invoice);
}

✅ Role-Based Access Control (RBAC / ABAC Integration)

If an administrator needs access to any invoice:

export async function getInvoiceSecure(invoiceId: string, currentUser: { id: string; role: string }) {
  // 1. If user is ADMIN, allow fetching without owner constraint
  if (currentUser.role === "ADMIN") {
    return await db.invoice.findUnique({ where: { id: invoiceId } });
  }

  // 2. Otherwise (REGULAR USER), enforce owner constraint
  return await db.invoice.findFirst({
    where: {
      id: invoiceId,
      userId: currentUser.id
    }
  });
}

4. Migration to Non-Predictable Cryptographic IDs (CUID2 / UUID v4)

❌ Vulnerable Prisma Schema (Auto-incrementing IDs easy to enumerate)

model Invoice {
  id Int @id @default(autoincrement()) // ❌ Ex: 1, 2, 3, 4... Trivial enumeration
}

✅ Secure Prisma Schema (Random CUID2 identifiers)

model Invoice {
  id String @id @default(cuid()) // ✅ Ex: clh1g98x200003567y8z9a0bc
  userId String
  user User @relation(fields: [userId], references: [id])
}

BOLA Audit Checklist for Dev & QA Teams

  • [ ] Audit all endpoints with URL parameters: Ensure GET /api/resource/:id, PUT, DELETE filter by authenticated userId.
  • [ ] Automated Authorization Testing: Write integration tests (Jest / Playwright) where User A attempts to access User B's resource and expects 404 / 403.
  • [ ] Disable auto-incrementing IDs: Use non-predictable UUID v4 or CUID2 identifiers.