Your APIs are doing more than ever. So are the people trying to break them.
A couple of months back, a friend of mine called me looking a bit shaken. He runs a small SaaS, maybe a thousand customers, nothing huge. Their API had been quietly leaking data for about three weeks before anyone noticed. Not a hack in the dramatic Hollywood sense. No firewall got smashed through. No zero-day exploit. Somebody had just figured out that one of their endpoints accepted a customer ID in the URL and didn’t bother checking whether the person making the request owned that ID. So they wrote a tiny script. Looped through IDs one through fifty thousand. Pulled down whatever came back.
What got me wasn’t the breach itself. It was how ordinary it was. No movie villain. No nation-state actor. Just a bored person with curl and a text editor. And here’s the thing that really sticks with me. With AI tools today, you don’t even need to bore someone with curl. You can ask a model to write the script, point it at any API, and walk away while it goes hunting.
That’s where we are now. APIs are the connective tissue of pretty much every product anyone ships. Microservices talk to each other through them. Mobile apps depend on them. Partners integrate through them. AI agents are starting to use them autonomously, which is its own can of worms. And the attackers have new tools too.
So let’s talk about what’s changed, and what you can do about it that doesn’t require a security budget you don’t have.
The world your API lives in now
A few years ago, when you said ‘API’, you usually meant a single backend serving a single frontend. Maybe a couple of mobile clients too. The traffic was mostly humans clicking buttons, the shape was predictable, and you had a decent sense of who your callers were.
That’s not the world anymore. Today your typical mid-sized product might have anywhere from ten to a hundred microservices, all calling each other constantly. There’s a public API for partners. There’s a mobile app. There’s a webhook system pushing events out to integrations. There’s maybe an internal automation that hits your endpoints from a queue worker. And now, increasingly, there are AI agents calling your API on behalf of users, making decisions you didn’t anticipate, and stringing together requests in ways no human ever would.
Each of these callers has different needs, different risk profiles, different ways of misbehaving. And every single one of them is a door into your system.
The old model of “I’ll just put authentication on it and call it a day” doesn’t really cut it anymore. You need to think about who’s calling, what they’re allowed to ask for, how often, in what patterns, and what to do when the pattern starts looking weird.
Why AI changed the game, even for people who aren’t using AI
I’ll be honest. A year ago I thought a lot of the “AI changes everything in security” talk was overhyped. It felt like every vendor was slapping AI onto their pitch deck. Then I watched a junior engineer at a meetup write, in about forty minutes, a passable API fuzzer using nothing but an LLM and a coffee. No security background. No exploit knowledge. Just curiosity and the ability to ask good questions.
That moment changed my mind. The barrier to entry for poking at APIs has collapsed. What used to require some skill and patience now requires the ability to type. Attackers don’t need to be experts anymore. They need an idea and access to a model.
And here’s the other side. AI agents are now legitimate callers of APIs. Tools like assistants and automations are out there in the wild, making API calls on behalf of real users. They sometimes make weird requests. They sometimes hallucinate parameters. They sometimes loop because they think they got the wrong answer the first time. Your API needs to be okay with this, but also not be easily tricked by it.
So you’ve got two trends pushing at the same time. The pool of people who can attack your API just got way bigger. And the pool of legitimate non-human callers also just got way bigger. Distinguishing between the two is the new game.
The five things that go wrong most often
After looking at way too many API breach post-mortems, I’ve started to notice the same handful of issues coming up over and over. The OWASP API Security Top 10 covers them formally if you want the official list, but here’s the plain-English version of the ones I see most.
- Broken object-level authorisation. This is the one my friend got hit by. Your endpoint takes an ID, returns the object, but doesn’t check whether the caller actually has the right to see that object. Embarrassingly common.
- Weak or missing authentication. Tokens that don’t expire. Tokens whose signatures aren’t being verified. Basic auth with hard-coded credentials still in use because it works and nobody wants to touch it.
- Excessive data exposure. Your endpoint returns the full user object including the password hash and the internal admin notes, and the frontend just shows the name. The frontend hiding field is not the same as the backend not sending them.
- Missing rate limits. An endpoint that can be called ten thousand times in a minute by a single client is essentially an open door for scraping, brute force, and denial of service.
- Outdated dependencies. Half the breaches I read about start with a library that had a known vulnerability which nobody patched. Boring. Preventable. Still happens constantly.
If you fix nothing else this quarter, fix these five. I mean it.
Authentication and authorisation are not the same thing
This trips up so many developers that it’s worth saying clearly. Authentication is who you are. Authorisation is what you’re allowed to do.
Authentication is mostly a solved problem at this point. OAuth 2.0, OpenID Connect, JWTs signed with proper algorithms. The libraries exist. The patterns are well known. You can mess it up, but the path to doing it right is clear.
Authorisation is where most apps fall apart. And it’s harder because it depends on your specific business logic. There’s no library that knows whether user 47291 should be allowed to update invoice 88420. That depends on whether user 47291 owns the invoice, or works for the company that owns it, or has been delegated permission, or any number of other things that are unique to your product.
Here’s the simplest version of what an authorisation check on a per-object basis might look like in a node service:
app.get(‘/api/invoices/:id’, authenticate, async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
if (!invoice) return res.status(404).send(‘Not found’);
if (invoice.ownerId !== req.user.id && req.user.role !== ‘admin’) {
return res.status(403).send(‘Forbidden’);
}
res.json(invoice);
});
It’s three extra lines. Three lines that prevent your API from becoming the next inventory script story. Yet I see endpoints without this check probably more often than I see endpoints with it.
The trick is to make this check impossible to forget. Either build a middleware that runs on every request and checks ownership, or push authorisation into a policy layer like Open Policy Agent so it’s centralised and reviewable. Whatever you do, don’t rely on every developer to remember every time.
Microservices made the problem harder, then easier, then harder again
When microservices first became the thing, a lot of teams ran into a weird security problem. Inside the cluster, services trusted each other completely. The reasoning was: we’re all behind the same firewall, so what could go wrong? Well, quite a lot, especially once attackers got a foothold in any one service and could pivot freely from there.
The industry’s answer to this has been mutual TLS and service meshes. Both ends of every internal connection authenticate each other cryptographically. Identity flows through the mesh transparently. You write your service like it’s calling plain HTTP, and the mesh handles the encryption and identity verification.
If you’re using Istio, Linkerd, or Cilium, you get a lot of this for free. Worth setting up if you haven’t. The first time you watch a service refuse to talk to another service that isn’t supposed to be calling it, you’ll wonder how you ever lived without it.
The harder part again is that microservices multiply the number of places where things can go wrong. Every internal endpoint is a potential entry point if an attacker manages to land somewhere in your cluster. So the same care you’d put into public endpoints needs to extend inward. No service should trust another service just because they share a network.
Rate limiting, the boring superpower
Nothing about rate limiting is exciting. Nobody puts “implemented sophisticated rate limiting” on their resume. And yet I would bet that more breaches and abuse incidents are prevented by a thoughtful rate limit than by any other single technique.
The idea is simple. Limit how many requests a given caller can make in a given window. If they exceed it, slow them down or reject them outright. This blunts pretty much every automated attack. Credential stuffing. Scraping. Enumeration. Brute force. All of them depend on being able to make a lot of requests quickly. Take that away and the attacker gets bored and moves on.
The trick is that rate limits aren’t one-size-fits-all. A user logging in might reasonably make five login attempts in a few minutes. An automation pulling reports might reasonably make a thousand requests in a few minutes. A public unauthenticated endpoint should be much more restricted than an authenticated one.
Tools like NGINX, Envoy, or cloud-managed gateways can do this for you. Or you can do it in your application using libraries like express-rate-limit for Node or django-ratelimit for Django. Wherever you put it, just put it somewhere.
A reasonable starting point might look like this in Express:
const rateLimit = require(‘express-rate-limit’);
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: ‘Too many login attempts. Please try again later.’
});
app.post(‘/api/login’, loginLimiter, handleLogin);
Five attempts per fifteen minutes for login. That single change makes credential stuffing impractical for most attackers.
Input validation and the lie of frontend safety
I’ve said this in the previous article (OSFY, June 2026) and I’ll say it again because it bears repeating. Frontend validation is a UX feature, not a security feature. Anyone with curl, or these days anyone with an LLM and ten minutes, can skip your beautiful form and hit your API with whatever shape of data they want.
Validate everything on the server. Use a schema validator like Zod, Joi, Yup, or Pydantic for Python. Define exactly what shape each endpoint accepts. Reject anything that doesn’t match. Don’t try to be clever. Don’t try to coerce weird input into the right shape. Just reject it.
This sounds obvious, but I see APIs that happily accept extra fields and stick them straight into a database insert without checking. That’s how you get mass assignment vulnerabilities, where a user sends { “name”: “Alice”, “isAdmin”: true } and your endpoint dutifully grants them admin rights because nobody filtered the input.
The pattern is simple. Define the shape you expect. Strip everything else. Trust nothing.
const userUpdateSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
preferences: z.object({
theme: z.enum([‘light’, ‘dark’]).optional()
}).optional()
}).strict();
That .strict() at the end is the magic. It rejects requests with unexpected fields, instead of silently passing them through.
Logging without leaking, monitoring without panicking
You need logs to figure out what happened. But logs can become a security problem if you’re not careful.
The most common mistake is logging request bodies wholesale. Some of those bodies contain passwords, API keys, credit card numbers, personal data. Once that ends up in your log aggregator, you’ve effectively expanded the scope of where that sensitive data lives. Now your logging system needs the same protections as your database.
A better approach is to decide ahead of time what’s worth logging. Method, path, status code, response time, the identity of the caller, any error code. Skip the body unless you’ve explicitly scrubbed it of sensitive fields. Some logging libraries have built-in redaction. Use it.
On the monitoring side, watch for patterns rather than individual events. A single 401 doesn’t mean anything. A thousand 401s from one IP in five minutes means something. A user account that suddenly starts hitting endpoints it has never hit before, especially in rapid succession, is worth a look. A spike in 500 errors on an endpoint that’s been quiet for months is interesting.
Tools like Datadog, Grafana, or Elastic Stack can help you set up these kinds of alerts. The key is to alert on patterns that are actually unusual, not just on raw thresholds, or you’ll drown in noise and stop paying attention.
Secrets, tokens, and the mess people make of them
I covered this in the zero-trust article (OSFY, June 2026), but it’s worth revisiting in the API-specific context because the patterns are slightly different.
API keys are still everywhere, and that’s mostly okay if you treat them like passwords. Rotate them. Don’t commit them to git. Use a secrets manager. Give each integration its own key so you can revoke them individually. Set expiry dates and enforce them.
For internal service-to-service calls, prefer short-lived tokens issued by an identity service over long-lived API keys. The pattern is similar to user authentication, just with services as the subject. SPIFFE-style workload identities are great for this if you’re at the scale where managing tokens by hand becomes a chore.
For partner APIs and public APIs, OAuth 2.0 with proper scopes is your friend. Define scopes that mean something. read:invoices and write:invoices are different things and should be granted separately. Don’t fall into the trap of giving every integration full access just because it’s easier.
And please, please, do not put API keys in mobile apps and call them secret. Anyone with a few minutes and the right tools can pull every string out of your APK or IPA. Mobile apps should authenticate through a user session, not through a shared secret embedded in the binary.
When AI agents are calling your API
This is the new wrinkle and it’s worth thinking about specifically. AI agents acting on behalf of users are starting to show up as legitimate callers. They’re going to make requests that look weird. They’re going to retry things. They’re going to hallucinate parameters and send you nonsense. They’re also going to be targeted by attackers trying to manipulate them into doing things they shouldn’t.
A few things help. First, treat agent traffic as a distinct caller category if you can. Issue tokens specifically for agent use, with scopes that reflect what an agent should be allowed to do on the user’s behalf. Don’t just give them the user’s full session.
Second, watch for unusual patterns. An agent that suddenly starts trying to access a different user’s data is either compromised or being manipulated. Either way, that’s worth alerting on.
Third, think hard about destructive actions. If an agent can delete records, transfer money, or send messages to other people, those actions should require additional confirmation or be subject to stricter rate limits. The cost of an agent making a mistake on a read endpoint is small. The cost of an agent being tricked into wiring money somewhere is not.
Prompt injection, where an attacker tries to manipulate an AI agent through cleverly crafted input, is a whole topic of its own. The short version is that your API can’t assume the agent is operating with clean inputs. Treat agent requests as semi-trusted, not fully trusted, and design accordingly.
Versioning, deprecation, and the long tail of old endpoints
Here’s a quiet problem that gets very loud at the worst times. You released version 1 of your API four years ago. You released version 2 two years ago. You released version 3 last month. You documented version 3. You forgot about version 1 entirely. And it’s still out there, still serving traffic, still using the authentication patterns you’ve since abandoned.
This is how a lot of breaches happen. Not through the shiny new endpoint. Through the forgotten old one.
Build deprecation into your API design from day one. Mark old versions clearly. Communicate end-of-life dates aggressively. Turn off endpoints when their time is up, even if it means breaking the one customer who never migrated. Especially if it means breaking the one customer who never migrated.
Audit your API surface regularly. Pull a list of every endpoint that’s receiving traffic. Compare it to what’s documented. Anything on the list that isn’t in the docs is a candidate for removal. Anything in the docs that isn’t on the list is a candidate for being formally deprecated.
What you can do this week, this month, this quarter
This week, pick your three highest traffic endpoints. Look at the authorisation logic. Make sure each one checks not just authentication but also whether the caller is allowed to do what they’re asking. If any of them just check “is this user logged in” and then return the requested object regardless of ownership, fix that first.
This month, audit your authentication. Are tokens actually being verified properly? Are signatures being checked? Are expiry times reasonable? Is there a way to revoke a token if it leaks? If the answer to any of these is “I’m not sure,” go find out.
Add rate limiting to your login endpoint, your password reset endpoint, and any endpoint that returns sensitive data or sends emails. Five per fifteen minutes is a reasonable starting point for human-facing endpoints.
This quarter, set up structured logging if you don’t have it. Ship the logs somewhere central. Set up alerts on the patterns that matter. Review your dependency tree for known vulnerabilities, ideally with an automated tool that runs on every build. Tools like Snyk, Dependabot, or npm audit do most of the work for you.
Then keep going. Security isn’t a project you finish. It’s a habit that gets a bit stronger every sprint.
The road ahead
A few trends worth keeping an eye on. AI-powered API testing tools are getting genuinely useful, both for defenders and attackers. Tools like StackHawk and similar are starting to use LLMs to generate test cases that explore weird edge cases human testers would miss. Worth experimenting with.
API gateways are getting smarter. Modern ones can do anomaly detection, automatically block suspicious patterns, and integrate with identity providers in ways that would have been a major engineering effort a few years ago. If you’re still rolling your own gateway logic, it might be worth seeing what’s available off the shelf.
GraphQL is everywhere now and it brings its own set of security headaches. Query complexity attacks, where someone sends a deeply nested query that takes your server forever to resolve, are common enough that you should specifically defend against them if you’re using GraphQL. Depth limits and query cost analysis are your friends.
And the regulatory environment is tightening. Data protection laws around the world are becoming more demanding about how you handle API access, especially for personal data. Building security in from the start is going to be more cost-effective than retrofitting it when the rules change.
Code that doesn’t trust anyone, including itself
The honest reality is that there’s no point at which your API is “secure” and you can stop thinking about it. Every new feature is a new surface. Every new integration is a new caller. Every new tool the attackers get is a new way in. The best you can do is build the habit of doubting your own code, validating every input, checking every permission, logging every meaningful action, and reviewing what you’ve shipped on a regular schedule. The teams that take this seriously rarely show up in the news. The ones that don’t eventually do. Pick a few habits from this article, especially the ones that apply most to what you’re building. Start this week. Your API doesn’t need to be perfect. It just needs to be a less appealing target than the next one. Most of the time, that’s enough.















































































