I want to do something slightly unusual with this article. I'm going to walk you through how I'd attack your company's API. Not your hypothetical API — the API of the company you actually work at, the one you signed an agreement to defend, the one your customers send their data to every minute.
The exercise is useful because most security writing tells you what to defend against. It rarely tells you what an attacker is actually thinking when they sit down at a keyboard and decide your company is interesting. The defender's lens and the attacker's lens see different things. I want you to see the second one for the length of this article. We'll switch back at the end.
One ground rule: nothing here describes hacking a system without permission. The exercise is a thought experiment in how an offensive engagement against your own company would actually play out. If you're a founder or CTO, treat this as a guided tour of what we do when you hire us. If you're an engineer, treat this as a reframing of the surface you've been looking at from the inside.
The first hour
I open my laptop, pour coffee, and type your company name into a browser. The first hour is about understanding the shape of the target. Marketing site, product pages, documentation, careers page (more useful than you'd think — your job postings tell me what languages you use, what cloud you're on, what your tech stack looks like). If you have an API, I want your developer documentation. Most companies publish it.
Your developer docs are a gift. They describe your authentication flow, your endpoints, your data models, your rate limits, your example payloads, your error messages. Every example I get to read is a free preview of your production behavior. I copy the curl examples into a working notebook. I now know how to make my first authenticated call to your service.
If you don't publish docs, I look for your mobile app. I open it in the Apple App Store or Google Play, download it, and run it on a test device with a proxy in between. Every screen the app renders sends an API call. I now have a complete list of your API endpoints, the parameters they accept, and the responses they return — gathered from the only public-facing client of your API that I needed.
This part — what we call "passive reconnaissance" — is over in two or three hours and produces enough material to fill a notebook. I have a map of your API. I have example payloads. I have a sense of your authentication model. I have a working test account from the free tier. I haven't touched anything that could be detected.
The second hour: looking for inventory drift
Now I start poking. The first thing I want to know is: are there endpoints that aren't in your documentation but exist anyway?
You'd be surprised how often the answer is yes. Old endpoints from previous API versions that never got removed. Debugging endpoints that were supposed to be internal but got exposed. Admin endpoints that the team thought weren't reachable from the public internet but, due to a load balancer config, are. Health-check endpoints with more information than they should have. Endpoints from the staging environment that mirror to production by accident.
I find these by enumeration. There are wordlists of common API paths — /admin, /debug, /internal, /v1/admin, /.git, /swagger.json, /openapi.json, /graphql, /graphiql, /__playground. I throw a few hundred of these at your API. Most return 404. A few return 200, or 403, or 401 — each of which is interesting.
What I'm hoping for, and what I find on roughly half of engagements, is one of:
- An old API version (
/v1when current is/v3) that lacks the security improvements the current version has. - A swagger.json or openapi.json file accidentally exposed in production, giving me the entire schema for free.
- A GraphQL endpoint with introspection enabled, which is the GraphQL equivalent of swagger.
- An admin endpoint at a guessable path with weaker authentication than the customer endpoints.
- A debug endpoint that returns stack traces, environment variables, or database query plans.
Each of these is a meaningful win for me. The swagger find is the best — it tells me about endpoints I didn't even know to look for, often including the internal-only ones.
The third hour: the IDs
Now I sign up for two accounts. I am Alice in one browser. I am Bob in another. I do a normal thing in Alice's account — create an invoice, upload a document, save a setting, whatever your API supports. I watch the request.
The request, almost always, has an ID in it. /api/invoices/8421. /api/documents/4f8e2d. /api/orgs/482/users/9911. The ID identifies the resource I'm acting on.
I switch to Bob's session. I take Alice's request, change Alice's session cookie to Bob's session cookie, and resend it. Bob is now requesting Alice's resource. The question is: does the API check whether Bob is allowed to do this?
The bug class is called BOLA — Broken Object Level Authorization. The OWASP API Security Top 10 lists it as the number-one risk for a reason. A 2025 industry analysis attributed 58% of all API-related security incidents to BOLA and its sibling BFLA. We find it on more engagements than not.
When it fires, the API serves Bob Alice's resource. Sometimes the resource is innocuous — a notification preference. Sometimes it is an invoice with another customer's payment details. Sometimes, in the worst cases, it is a user-management endpoint, and the response tells me Bob can now modify Alice's user record. That last case is how Uber lost user data in 2016, how Facebook lost it in 2018, and how Trello lost it in 2024. It is also how, in May 2024, a single API vulnerability at Dell let an attacker exfiltrate 49 million customer records by manipulating fake accounts.
The third hour is, for me, the most productive. It's where the chain to data exfiltration usually starts.
The afternoon: mass assignment and excessive exposure
By now I have a working understanding of your API's authentication and authorization model. I move on to the input layer.
I send Alice's profile update endpoint a payload with extra fields. The endpoint expects { "name": "Alice", "bio": "..." }. I send { "name": "Alice", "bio": "...", "role": "admin", "tenant_id": 1, "is_verified": true, "credit_balance": 99999 }.
This bug is called mass assignment. The framework, helpfully, sets every field I send. If the team built the endpoint with "update_attributes(params)" instead of an explicit allowlist, every field on the user record is fair game. I'm now an admin. Or a verified user. Or have a 99,999-credit balance. Or a member of tenant 1 — which is, often, the founder's tenant.
The fix is explicit allowlists at the input layer. Every modern framework supports them. Not every developer remembers to use them. I'd estimate I find mass assignment on roughly one in three engagements that include a custom-built API.
Then I look at responses. Alice asks for her profile. The API returns { "id": 8421, "name": "Alice", "email": "alice@example.com", "password_hash": "...", "internal_notes": "VIP customer", "api_key": "sk_live_...", "stripe_customer_id": "cus_..." }. The frontend only renders her name and email. The API returned everything. The team's mental model is "the UI hides the rest." The attacker's mental model is "the API returned it, so it's mine."
This is excessive data exposure. The fix is at the API layer — explicit allowlists on what gets serialized into responses. The mistake is "we'll filter in the UI." The UI is the part the attacker isn't using.
The afternoon, continued: the rate limit that wasn't
While I'm there, I'll check for rate limits on the endpoints that matter most. I open a terminal and write a small script. The script tries to log into Alice's account with passwords from a leaked breach list. I run it.
I'm expecting to be rate-limited after a few attempts. On a well-built API, I am. On a typical API, I'm not. The endpoint accepts thousands of requests a minute without complaint. If Alice's password is in any of the major breach lists — and statistically, it probably is — I'll have her credentials before lunch.
The same exercise on the password-reset endpoint. The same exercise on the magic-link-request endpoint. The same exercise on the "send email confirmation" endpoint, which I can use to flood Alice's inbox and burn through your transactional email budget.
Rate limits should exist on every endpoint that authenticates a user, costs money, sends an email, or makes an outbound network call. Most APIs have rate limits on at most one of those categories. We routinely see APIs that allow brute-force login at rates that would have been considered negligent in 2010.
The pivot: GraphQL changes the math
If your API is GraphQL, my attack changes shape. GraphQL is honestly a remarkable tool for product velocity — your engineers can ship features faster because the API doesn't need a new endpoint for each new view. But it ships with two structural problems that REST doesn't have to the same degree.
The first is query depth. GraphQL lets a client ask for nested data: "give me this user, and their organization, and their organization's members, and each member's projects, and each project's tasks, and each task's comments, and each comment's author's organization..." A nesting depth of twelve is not unusual in legitimate queries. A query of depth fifty is what I'd write if I wanted to bring your database to its knees. Some implementations have no depth limit by default. I send the query. Your database falls over. This is, technically, a denial-of-service finding. Practically, it's a way to make your engineering team angry at me while I look around.
The second is field-level authorization. In REST, an endpoint exists or it doesn't. In GraphQL, every field is potentially queryable, and "is this user allowed to read this field?" has to be answered per-field. If your team built the GraphQL schema with global access controls instead of field-level ones, I can probably ask for fields the UI never exposes — internal notes, audit fields, related-user data, the kinds of things your team didn't think were reachable.
The GraphQL-specific bug we find most often: introspection enabled in production. Introspection lets me ask the API "describe yourself." It returns the entire schema, including hidden fields, mutation types, and internal model relationships. It is the GraphQL equivalent of leaving your blueprints on a public S3 bucket. There is almost no legitimate reason to leave it on in production. We find it on, conservatively, half the GraphQL APIs we test.
The end of the day: chains
By close of day one, I have a notebook full of findings. None of them, individually, looks catastrophic. A mass-assignment field. A BOLA in an admin endpoint. An exposed swagger. A weak rate limit. An over-permissive GraphQL query. A logging endpoint that leaks stack traces.
This is where the attacker mindset diverges most from the defender mindset, and where I want you to pay attention. The findings only matter as chains.
The mass-assignment lets me set my tenant_id to 1. The exposed swagger tells me tenant 1 has an admin-only endpoint at /api/v3/admin/customers. The BOLA on that endpoint lets me query any customer record. The over-permissive GraphQL query lets me dump all customers at once. The weak rate limit lets me dump them quickly. The leaky stack trace tells me where the production database is and what the connection string looks like.
Now I have your entire customer database. None of the individual bugs was a "data breach." Together, they are. The 2025 Cybelangel API Threat Report notes that the average time to detect an API breach is 212 days longer than for general network breaches. Your detection stack, if it caught anything at all, will catch it long after I'm done.
The story your post-mortem will tell
The breach hits the news. You write the post-mortem. The post-mortem talks about "a vulnerability in our customer-management API." It does not, usually, talk about the four other bugs I chained to reach it, because the team writing the post-mortem doesn't always reconstruct the full chain — they fix the first bug they find and move on.
This is the version of API breaches you see in the news. T-Mobile 2023: 37 million records via a misconfigured API. Dell 2024: 49 million records via API authorization flaws. The Dropbox API key breach in May 2024. The Stripe legacy-API hijack that ran from August 2024 through 2025 with attackers processing fraudulent payments via card skimming on at least 49 e-commerce websites. The xAI API key leak in 2025. The OpenAI / Mixpanel third-party API breach in November 2025.
The post-mortems vary in length and quality. The pattern across them is the same. An API endpoint did something the team didn't think it could do, to data the team didn't think was reachable, at a rate the team didn't think was possible. Each one of those is a layer that, if defended, would have stopped the chain.
Now switch sides
You've followed me through a day of attacking your company. Now you sit at the same desk, in your actual job, and ask: what would have stopped me?
The first hour — passive reconnaissance — you can't stop. Your docs are public on purpose. Your mobile app is downloadable. I'll always get the map. What you can do is make sure the map is the only map. No hidden endpoints. No accidentally-exposed swagger. No GraphQL introspection in production. No staging endpoints accessible from the public internet. The principle: if it exists in your API, it should be documented; if it shouldn't be documented, it shouldn't exist.
The third hour — the IDs — is where the highest-impact defensive work lives. The right architectural choice is to put authorization at the data-access layer, not the controller layer. Every query for an invoice asks "by this user?" at the moment of access. Every endpoint becomes incidental to the authorization model. New endpoints inherit the model for free. The wrong choice — putting authorization in each controller — guarantees that the next endpoint you ship will be the one someone forgot. We see this often enough to call it the single highest-leverage architecture decision in API security.
The afternoon — mass assignment, excessive exposure, rate limits — is process. Explicit allowlists on input. Explicit allowlists on output. Rate limits on every endpoint that authenticates, costs money, or makes outbound calls. None of this is hard. All of it requires someone to actually do it on every endpoint, including the ones added during last week's feature push.
GraphQL specifically: turn off introspection in production. Set a query-depth limit. Implement field-level authorization, not just schema-level. Use persisted queries if you can — your frontend's queries are known in advance; only those should be allowed.
The chain awareness: this is the cultural piece. Your team's reaction to a finding shouldn't be "what bug let this happen" but "what other bugs would have been needed for this to escalate to a breach, and do we have those too?" The mass assignment by itself is a low-severity finding. Combined with an exposed swagger and a BOLA, it's a critical chain. The bugs only matter together.
If your last security test was for "the web app" and ignored the API — or treated the API as a fifteen-minute scan with an automated tool — your test missed the part of your system that processes the actual transactions. The web frontend is a presentation layer. The API is the entire business.
One last thought from the wrong side of the keyboard
The reason API security is so productive for attackers is that the layer is invisible to most product teams. Engineers see the React frontend, the mobile app, the marketing site. Product managers see the user-facing features. The API — the actual contract, the actual surface, the actual code that sees user data — is what the team gets to ignore because it "just works" once the frontend renders correctly.
From my side of the keyboard, the invisible layer is the entire game. Your customers' data lives there. Your business logic lives there. Your authentication checks live there. Every bug I described above is in code your team writes and your team owns. None of them are in a third-party dependency or a vendor you can blame. They are choices your team made, often without noticing they were making them.
The way out — and this is the part I want you to take away — is to look at the API the way I look at it. Not as the thing the frontend talks to. As the actual product. As the actual attack surface. As the layer that 57% of organizations have already been breached through in the last two years, per recent industry surveys, and where the next one will come from too.
That reframing — if you make it — does most of the work. The rest is the engineering.