• 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/API9: Improper Inventory Management
API9CWE-1059

The old version of the API that nobody turned off
Improper Inventory Management

/api/v1 still works. It has the bug you fixed six months ago in /api/v2.

The quiet finding — nobody lists it on a pentest, and everyone has it.

Reference: API Top 10 (2023) — API9·Last updated April 7, 2026·By Flowpatrol Team
Improper Inventory Management illustration

You shipped a fix. You cut a new version. You moved the frontend. The old version is still live because nothing tells you how to turn it off, and one customer on an old mobile build is still calling it. Six months later, an attacker finds both versions and picks the one that still has the bug.

Improper Inventory Management is the bug where you do not know what you have running. Old API versions that were never turned off. Staging and beta subdomains pointed at prod data. Undocumented routes from a prototype nobody deleted. Every one of them is a live attack surface the team has stopped watching.

What your AI actually built

You asked for a versioned API. /api/v1 for the original launch, /api/v2 once you added multi-tenant support and fixed that nasty ownership bug. The model did exactly what you asked — it created v2 alongside v1.

What it did not do was retire v1. It did not put a 410 Gone on it, did not add a deprecation header, did not log which clients were still hitting it. v1 sits in the repo looking exactly like v2 except for the one thing you fixed.

The other flavor of this bug is the staging server. A subdomain called beta. or dev. or internal. that points at a slightly older build of the same app, often with looser auth, debug endpoints on, and the same database behind it.

How it gets exploited

The attacker notices the main app is careful: every route checks ownership, every token is scoped. They assume there is an older version somewhere.

Find the versions you forgot were live.

Flowpatrol walks every version, subdomain, and shadow route of your app and shows which ones still have the bugs you already fixed. Five minutes. One URL.

Try it free
1
Look for ghosts
They try /api/v1, /api/legacy, and api-staging.example.com. /api/v1/invoices/123 responds — with the IDOR that v2 no longer has.
  • 2
    Enumerate
    A script walks /api/v1/invoices/1 through 20000. Every invoice comes back, because v1 was never scoped to the session user.
  • 3
    Pivot to the staging host
    staging.example.com runs the same app against the production database. The staging build still has /api/debug/sql enabled. One query dumps the users table.
  • 4
    Walk into prod
    The leaked users table has session tokens from yesterday. Half of them are still valid. The attacker is now logged in as real production users.
  • The production app was clean. The production-adjacent surface was not. Nobody on the team could have told you staging was still pointing at the prod database — it had been that way since launch and nobody changed it.

    Vulnerable vs Fixed

    Vulnerable — old version still live, same data, old bugs
    // app/api/v1/invoices/[id]/route.ts  (shipped 2024, never retired)
    export async function GET(req, { params }) {
      // Original ownership bug — no session check.
      const invoice = await db.invoice.findUnique({
        where: { id: params.id },
      });
      return Response.json(invoice);
    }
    
    // app/api/v2/invoices/[id]/route.ts  (the "fixed" version)
    export async function GET(req, { params }) {
      const session = await getSession(req);
      const invoice = await db.invoice.findUnique({
        where: { id: params.id, userId: session.user.id },
      });
      return Response.json(invoice);
    }
    Fixed — v1 is gone, not just quiet
    // app/api/v1/[...all]/route.ts
    export async function GET() {
      return new Response(
        JSON.stringify({
          error: 'API v1 retired on 2025-01-15. Use /api/v2.',
        }),
        {
          status: 410,
          headers: {
            'Content-Type': 'application/json',
            'Deprecation': 'true',
            'Sunset': 'Wed, 15 Jan 2025 00:00:00 GMT',
          },
        },
      );
    }
    export const POST = GET;
    export const PUT = GET;
    export const DELETE = GET;
    export const PATCH = GET;

    Deleting routes from the repo is not enough if a build is still deployed somewhere. Replace every v1 route with a single catch-all that returns 410 Gone, a Sunset header, and a clear pointer to v2. Do the same thing for staging and beta subdomains — if they do not need to exist, they should return 410, not a working response.

    A real case

    USPS — 60 million records leaked via an old, forgotten API

    In 2018, a researcher found an undocumented "Informed Visibility" API on usps.com that let any logged-in user query any other account — the endpoint existed for a year before anyone noticed, because nobody had an inventory of what was live.

    Related reading

    Glossary

    Broken Object Level Authorization (BOLA)Insecure Direct Object Reference (IDOR)

    What we find

    shadow endpoints

    References

    • API9: Improper Inventory Management — official OWASP entry
    • OWASP API Security Top 10 (2023) — full list
    • CWE-1059 on cwe.mitre.org