Technical session led by Siri Varma Vegiraju, Tech Lead at Microsoft Azure Security, for Offensive Engineering.
I have worked on the security side of things for the last four to five years, and one thing still surprises me. Working on the services side, you assume customers do all of this end to end. Multi-factor authentication, authorization checks in the right places, the full set. Then I go to a customer and find none of it in place.
That gap is why I keep running these sessions. They educate me as much as anyone going through them, and the point is to give people enough to bolster their own services so their customers can trust them.
The OWASP API Top 10 has been around a while now, but I prefer to revisit it on a cycle because things keep changing here. With generative AI and the wider machine learning space growing quickly, the way people build and expose APIs changes with them. So this covers the same list from where it stands today.
There are five areas to get through. Authorization and authentication, resource starvation and abuse prevention, server side request forgery, asset management, and security logging and monitoring. Where a failure comes down to a few lines of code, I have put the vulnerable version beside the corrected one.
Here are the slides along with the session recording.
Authentication and authorization answer two different questions
Every API you deal with needs both, and they are two different things.
Authentication asks whether you are even allowed into the system. Authorization asks whether you hold permission to do the thing you are attempting once you are inside. Most of the code failures below come from confusing the two or from doing one and skipping the other.
OWASP breaks the authorization side down further into object level, object property level, and function level. Each one fails differently, so each gets its own example.
Roles taken from the request body hand out admin access
Imagine you offer an API where a customer updates their own record. This is a POST, so it carries a body, and the handler reads what it needs from there.
@app.route('/api/users/update', methods=['POST'])
def update_user():
data = request.json
user = get_current_user()
user.email = data.get("email")
user.role = data.get("role")
db.save(user)
return jsonify({"status": "updated"})
Taking the email from the body is fine. Taking the role is not.
A role is your way of saying I am an admin, I am a user, I am someone else. Roles should always come from the authorization token, which the user cannot modify. Because this handler reads the role from the request body, a threat actor puts themselves down as admin and hits save. Someone who was supposed to hold read-only access now holds admin privileges.
The correction removes the assignment entirely.
@app.route('/api/users/update', methods=['POST'])
def update_user():
data = request.json
user = get_current_user()
user.email = data.get("email")
db.save(user)
return jsonify({"status": "updated"})
When you fetch the current user, you already have their role from whatever you configured during onboarding. Take it from there rather than from the request.
That rule holds beyond this one endpoint. Anything the user should not have access to, role being the obvious case, should never come from user-level properties. It should come from somewhere your system already trusts.
Decoding a token proves nothing
The next endpoint serves a profile. A token arrives, I decode it, and I return a response.
const jwt = require('jsonwebtoken');
app.get('/api/profile', (req, res) => {
const token = ...
const decoded = jwt.decode(token);
const user = getUserById(decoded.sub);
res.json(user);
});I am not doing anything with that token beyond reading it, and that is broken authentication. It is a real bug, and it shows up in production more often than it should.
There is a second failure hiding in the same handler. If I have a profile, Alice should not be able to read it. An endpoint that accepts any decoded token and looks up whatever subject it names will serve one user’s data to another.
The fix pulls the token from the authorization header, verifies it against the signing key, and refuses anything that fails.
const jwt = require('jsonwebtoken');
app.get('/api/profile', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = getUserById(decoded.sub);
res.json(user);
} catch (err) {
return res.status(401).send();
}
});
Verification covers both the signature and the expiry, so a forged or stale token stops here rather than reaching your data layer. Decoding alone tells you what a token claims. Validating the fields in it is what makes your authentication code work correctly.
Function level checks decide who can call the endpoint
Property level asks whether a user can modify a given attribute. Function level asks something coarser, which is whether the user can perform the operation at all.
@app.route('/api/users/delete', methods=['POST'])
def delete_user():
user_id = request.json.get('id')
db.delete_user(user_id)
return jsonify({"status": "deleted"})This delete API allows anybody to call it, which is almost never what you want. In most systems only an admin should be able to remove a user.
@app.route('/api/users/delete', methods=['POST'])
def delete_user():
user_id = request.json.get('id')
if current_user.role != 'admin':
return jsonify({"error": "Forbidden"}), 403
db.delete_user(user_id)
return jsonify({"status": "deleted"})
Read the current user’s role, refuse anyone who is not an admin, and only then perform the delete. That check is the whole of function level authorization.
Four practices that keep authorization honest
Whatever your individual handlers look like, four things belong in place around them.
Start with multi-factor authentication. Passwords are becoming more outdated as other mechanisms mature, and passkeys and authenticator apps cover the same ground far more robustly, so there is little reason to carry the weaker option.
Then validate your tokens properly, and I mean validation rather than decoding. Check the signature, check the expiration field, and check the audience claim.
Give people as little as you can get away with. Read access is enough for most work, and if someone needs to update something, write down the scenario that justifies it. An admin updating a record makes sense. A user updating something they did not create needs an explanation, and whatever you grant should be audited.
Regular access reviews come fourth and follow from the third. Every six months, ask whether the access is still needed. Anyone who does not revalidate loses it, which keeps privilege from piling up across a long tenure.
Resource starvation turns availability into a security problem
Say I am hosting an e-commerce API. People around the world use it, they are my actual customers, and everything works.
Then a threat actor decides to run a denial-of-service attack. They send millions of requests from different machines at my hosts and overload the API servers. Those servers become so busy that they cannot serve the traffic that is actually valid, and now I have downtime.
The cost is not only the outage. Service level agreements commit you to giving customers a working piece of functionality, and missing that commitment carries financial repercussions. Failures also cascade, so one saturated service takes dependent ones down with it.
Rate limiting at the gateway does most of the work
Most of this gets handled before the traffic reaches your code. Cloudflare and similar providers offer CDNs and gateways, and the gateway is where rate limiting belongs.
Rate limiting works in two common ways. One is capacity based. If you run ten machines and each handles around a thousand requests per minute, anything beyond that gets denied rather than queued. The other keys off identity, where you attach an identifier to each request source and limit sources differently. A customer on a lower tier gets a tighter ceiling, and so does a source that looks genuinely threatening.
Monitoring covers what the limits miss. With enough telemetry coming in, spiky traffic becomes visible and you can root cause it while it happens. If the spike turns out not to be genuine, you block the source. Alerts running continuously against incoming traffic protect the system through constant auditing rather than after-the-fact investigation.
For enforcement, use a third-party product, or if you are already in cloud, use the gateways and rate limiters available there to enforce quotas and block traffic you do not want.
Server side request forgery reaches services that were never public
I covered some of this during my control plane session, and it is worth restating.
Say I am hosting a web server. That server normally holds access to a set of internal APIs. If I run an e-commerce site, I need inventory information and availability information, so I have routes to the internal services that serve them. None of those internal services should be publicly accessible, and that is the important part.
With an SSRF attack, a threat actor gets into that path and reaches the internal services through it. That is all server side request forgery is.
How the EC2 metadata attack worked
The best-known case involved EC2 instance metadata. Every EC2 instance carries a metadata API describing what the machine is and which roles it holds, and that API also serves authorization tokens for communicating with other services. Back in 2018 and 2019, you could reach it without any authorization at all.
The target service offered webhook functionality. The attacker configured a webhook to call the internal metadata API, and because the calling server ran inside the data center, the call went through and reached the authorization API.
What a webhook normally does is post data somewhere outside. So it posted the authorization tokens to an external endpoint where the attacker had a service waiting to record them. With those tokens, they reached third-party services, server details, object storage, and private credentials.
That was a lot at once, so here is the short version. A service holds access to internal APIs. A threat actor configures a webhook to reach those APIs and post the results to their own server.
Attacks in this shape are common, and several land every year.
Three controls stop most SSRF
Start with input validation and sanitisation. At the crudest level, a URL that names localhost and comes from outside gets refused. More broadly, no call you make and no data you record from a customer should end up reaching your internal services.
Segment the network next. Region-by-region isolation is common, so a compromise in one region stays contained there. You can go further and segment by service function, where an inventory management service reaches inventory APIs and inventory databases and nothing beyond. Someone who does get in then reaches two or three resources instead of everything you run.
Then keep allow lists. When customer-supplied data drives an outbound call, check the destination against a list of permitted URLs and deny everything else. A third-party URL gets to do one, two, three specific things and no more.
Between them, those three cover 80 to 85 percent of server side request forgery attacks.
Shadow APIs multiply faster than anyone tracks them
Companies run millions of assets. Virtual machines, object storage accounts, logging APIs, metering APIs, and a long tail beyond that. Knowing what your inventory holds is what lets you patch it, revisit it, and decide what to keep.
API proliferation has made this harder, and the reason is straightforward. With the rise of generative AI and LLMs, writing an API has become very easy.
So a database that should not carry a public route gets one anyway. The team frames it as a short-term measure, to be removed once the proper path exists. That removal never happens. Once you take a dependency on a system, moving away from it becomes a migration, which is close to impossible in practice.
APIs accumulate this way, and without a central record you lose track of what exists. That is where vulnerability management breaks down, because forgotten services carry old patches and old vulnerabilities while nobody looks at them, and sensitive business data stays reachable through them.
Compliance breaks the same way. An API nobody remembers is an API nobody audits, so you hold no record of who accessed it or what data moved through it.
Gateways and discovery tools make inventory possible
Getting all of this back under control takes three things working together.
Automated discovery tools scan your environment continuously and report the assets they find, which covers everything that was never documented in the first place.
Gateways handle the rest by policy. Block everything by default, and permit external exposure only through the gateway. API gateways carry Swagger and API management information, and because every call passes through them, auditing happens without additional work.
Management platforms then surface what discovery finds on a dashboard. A team goes through it entry by entry and decides what stays.
So the short version is this. Do not write APIs simply because they are easy to write, because the contract outlives the convenience. Expose everything through gateways so auditing becomes automatic. Use discovery tooling to keep asset information in one place a team can actually review.
Logging exists so someone can answer who deleted it
Every control plane API needs auditing, and I mean every one. Create, read, update and delete operations all belong in the record.
The reason becomes obvious the first time a customer arrives and says somebody deleted their resource and they want to know who. That audit information is the only thing that answers them.
Centralised logs are how you implement it. Log every control plane operation, store them, and expose that record to the customer so they can investigate a discrepancy on their own rather than opening a ticket for it.
Anomaly detection needs a response path behind it
Anomaly detection covers what nobody thought to alert on. An API steadily serving a thousand requests per minute that jumps to ten thousand has changed behaviour, and the change itself is the signal. Machine learning models do this pattern work well, flagging the case that does not fit rather than waiting for a threshold someone set in advance.
Detection on its own does not get you very far. Once you have caught the anomaly, you need a workflow that says what happens next. If a business-sensitive API shows a spike, blocking it is often the better call than letting the traffic through on the chance it is legitimate.
Write those steps down so that anything falling into a given bucket triggers a known action. Because the steps repeat, they automate cleanly. Monitoring, detection, and incident response wired together give you a system that either acts on its own or notifies you for investigation.
Three things worth keeping from all of this
Get the API controls right before anything else. The authentication and authorization story has to hold together first, because nothing you build on top of it will hold if it does not.
Use the tooling you already have. Gateways and monitoring control what reaches the outside and keep the rest inside your organisation.
And keep monitoring. Security is not something you do once and forget. It is a continuously evolving story, so you need enough automation to act when an anomaly appears, or at minimum to notify you so it can be investigated further.
Siri Varma Vegiraju is a Tech Lead on Microsoft Azure Security and a contributor to InfoSec Relations. We also interviewed him on attacking the cloud control plane earlier this year. Offensive Engineering publishes technical walkthroughs from practitioners in hands-on security roles.





