• Agents
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Guides
  • Blog
  • Docs
  • OWASP Top 10
  • Glossary
  • FAQ

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Home/OWASP Top 10/API Top 10/API3: Broken Object Property Level Authorization
API3CWE-213CWE-915

The API returns fields you never meant to send
Broken Object Property Level Authorization

When your GET endpoint leaks admin flags and your PATCH endpoint lets users write them.

Property-level authorization bugs appear whenever an ORM is serialized straight to JSON — which is most of the time.

Reference: API Top 10 (2023) — API3·Last updated April 7, 2026·By Flowpatrol Team
Broken Object Property Level Authorization illustration

You asked the model for a route that returns the current user. It wrote `return user`. That one line is both sides of the bug: every property on the row flows out on GET, and every property on the row flows in on PATCH. Attackers read the fields they shouldn't see and write the fields they shouldn't touch.

API3 covers two sides of the same mistake: exposing properties the caller shouldn't see, and letting them write properties they shouldn't touch. The database row and the API contract are not the same thing. Bugs happen when developers treat them as if they are.

What your AI actually built

The model spread the user record into the response. Email, name, avatar — but also passwordHash, stripeCustomerId, internal notes, isAdmin, role, and an internal twoFactorSecret field somebody added last week. The API sends all of it, because nothing filters.

On the write side, the update route does `db.user.update({ where: { id }, data: req.body })`. Whatever the client POSTs lands in the database. Send `{ role: "admin" }` in the body and congratulations, you are one.

Both halves come from the same root cause: the API treats the database row as the API contract. There is no separate shape for 'what this caller is allowed to read' or 'what this caller is allowed to write.'

How it gets exploited

A normal user signs up, logs in, and loads their profile page with the dev tools open.

1
Read the leak
The /api/me response has 40 fields. Most are boring. Three are not: role, stripeCustomerId, and passwordResetToken. The attacker copies them.
  • 2
    Turn the leak into a write
    They notice the app also has PATCH /api/me for updating display name and avatar. They send the same endpoint with { "role": "admin" } in the JSON body. The response comes back 200 OK with role: "admin".
  • 3
    Walk through the front door
    They refresh the page. The admin panel renders because the frontend trusts the role field. Every admin-only endpoint now returns real data because the backend also trusts it.
  • The attacker escalated from signup to admin in three HTTP requests, and never touched a single exploit. The root cause is a spread operator the model wrote because the prompt said 'update the user.'

    Vulnerable vs Fixed

    Vulnerable — spread the body straight into the database
    // app/api/me/route.ts
    export async function GET(req) {
      const session = await getSession(req);
      const user = await db.user.findUnique({ where: { id: session.userId } });
      return Response.json(user); // every column, including role and secrets
    }
    
    export async function PATCH(req) {
      const session = await getSession(req);
      const body = await req.json();
      const updated = await db.user.update({
        where: { id: session.userId },
        data: body, // attacker sends { role: 'admin' }, server obeys
      });
      return Response.json(updated);
    }
    Fixed — explicit read and write shapes
    // app/api/me/route.ts
    import { z } from 'zod';
    
    const PublicUser = z.object({
      id: z.string(),
      email: z.string(),
      name: z.string().nullable(),
      avatarUrl: z.string().nullable(),
    });
    
    const UpdateUser = z.object({
      name: z.string().max(120).optional(),
      avatarUrl: z.string().url().optional(),
    });
    
    export async function GET(req) {
      const session = await getSession(req);
      const user = await db.user.findUnique({ where: { id: session.userId } });
      return Response.json(PublicUser.parse(user));
    }
    
    export async function PATCH(req) {
      const session = await getSession(req);
      const data = UpdateUser.parse(await req.json());
      const updated = await db.user.update({ where: { id: session.userId }, data });
      return Response.json(PublicUser.parse(updated));
    }

    Two schemas: one for 'what the public sees' and one for 'what the owner can change.' Everything that is not in the allow list is dropped. Validation is not for formatting — it is the authorization boundary between the database and the API.

    A real case

    Parler's public API returned deleted posts, private messages, and GPS coordinates

    In 2021, researchers pulled 70TB of Parler data by hitting public API endpoints that returned every field on every post — including precise geolocation metadata and data the UI had marked 'deleted.'

    Related reading

    Glossary

    Unprotected Object Binding (Mass Assignment)Broken Object Level Authorization (BOLA)

    What we find

    broken access control

    References

    • API3: Broken Object Property Level Authorization — official OWASP entry
    • OWASP API Security Top 10 (2023) — full list
    • CWE-213 on cwe.mitre.org
    • CWE-915 on cwe.mitre.org

    Catch the fields your API shouldn't be sending — or accepting.

    Flowpatrol reads every response, replays every write, and flags the properties that cross the authorization line.

    Try it free