DEV Community

Atlas Whoff
Atlas Whoff

Posted on

OWASP API Security Top 10 for Next.js Developers: BOLA, SSRF, and Auth Bypass

What OWASP Top 10 Means for API Developers

The OWASP API Security Top 10 is not the same as the original Top 10.
APIs have different attack surfaces. Here's what each one means in practice for a Next.js API developer.

API1: Broken Object Level Authorization (BOLA)

// VULNERABLE -- anyone can read anyone's data
// GET /api/orders/[id]
export async function GET(req: Request, { params }: { params: { id: string } }) {
  const order = await db.order.findUnique({ where: { id: params.id } })
  return Response.json(order) // Returns any order to any authenticated user!
}

// FIXED -- verify ownership
export async function GET(req: Request, { params }: { params: { id: string } }) {
  const session = await getServerSession(authOptions)
  if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 })

  const order = await db.order.findFirst({
    where: { id: params.id, userId: session.user.id } // Scoped to current user
  })
  if (!order) return Response.json({ error: 'Not found' }, { status: 404 })
  return Response.json(order)
}
Enter fullscreen mode Exit fullscreen mode

API2: Broken Authentication

// VULNERABLE -- weak token
const token = Buffer.from(`${userId}:${Date.now()}`).toString('base64')
// Predictable, not signed, can be forged

// FIXED -- signed JWT
import { SignJWT } from 'jose'
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
const token = await new SignJWT({ userId })
  .setProtectedHeader({ alg: 'HS256' })
  .setExpirationTime('1h')
  .sign(secret)
// Or just use NextAuth -- it handles this correctly by default
Enter fullscreen mode Exit fullscreen mode

API3: Broken Object Property Level Authorization

// VULNERABLE -- returns all fields including sensitive ones
const user = await db.user.findUnique({ where: { id } })
return Response.json(user) // Includes passwordHash, stripeCustomerId, etc.

// FIXED -- select only what's safe to expose
const user = await db.user.findUnique({
  where: { id },
  select: { id: true, name: true, email: true, plan: true, createdAt: true }
  // Excludes: passwordHash, stripeCustomerId, internalNotes
})
Enter fullscreen mode Exit fullscreen mode

API4: Unrestricted Resource Consumption

// VULNERABLE -- no limits
const items = await db.item.findMany() // Returns 10,000 records

// FIXED -- pagination + limits
const { page = 1, limit = 20 } = SearchSchema.parse(searchParams)
const items = await db.item.findMany({
  take: Math.min(limit, 100), // Hard cap at 100
  skip: (page - 1) * limit,
})

// Also: rate limit expensive operations
// Also: limit file upload sizes (next.config.js sizeLimit)
// Also: timeout slow operations
Enter fullscreen mode Exit fullscreen mode

API5: Broken Function Level Authorization

// VULNERABLE -- admin functions accessible to regular users
// DELETE /api/admin/users/[id]
export async function DELETE(req: Request, { params }) {
  const session = await getServerSession(authOptions)
  if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 })
  // Checks auth but not admin role!
  await db.user.delete({ where: { id: params.id } })
}

// FIXED
if (session.user.role !== 'admin') {
  return Response.json({ error: 'Forbidden' }, { status: 403 })
}
Enter fullscreen mode Exit fullscreen mode

API7: Server Side Request Forgery (SSRF)

// VULNERABLE -- fetches any URL the user provides
const { url } = await req.json()
const response = await fetch(url) // Could hit internal services!

// FIXED -- validate URL before fetching
import { z } from 'zod'
const Schema = z.object({
  url: z.string().url().refine(url => {
    const { hostname, protocol } = new URL(url)
    return protocol === 'https:' &&
           !['localhost', '127.0.0.1', '0.0.0.0', '169.254.169.254'].includes(hostname) &&
           !hostname.startsWith('192.168.') &&
           !hostname.startsWith('10.')
  }, 'Invalid URL')
})
Enter fullscreen mode Exit fullscreen mode

API8: Security Misconfiguration

// Common misconfigurations:
// 1. Detailed error messages in production
catch (error) {
  // WRONG:
  return Response.json({ error: error.message, stack: error.stack }, { status: 500 })
  // RIGHT:
  logger.error({ error }, 'Internal error')
  return Response.json({ error: 'Internal server error' }, { status: 500 })
}

// 2. Debug endpoints in production
if (process.env.NODE_ENV !== 'production') {
  // Only expose debug routes in dev
}

// 3. Missing security headers
// next.config.js headers() -- add CSP, HSTS, X-Frame-Options
Enter fullscreen mode Exit fullscreen mode

MCP APIs and OWASP

MCP servers expose function-like APIs. BOLA, auth bypass, and resource consumption are all relevant attack surfaces -- the MCP Security Scanner checks for all of them.

$29/mo at whoffagents.com

Top comments (0)