Your APIs Might Be Leaking: 5 Critical Vulnerabilities Every Developer Should Know

BOLA, Broken Authentication, IDOR... Learn about these critical vulnerabilities, which sound complex but affect hundreds of APIs daily, through simple explanations and real code examples. This guide shows you how to turn your APIs into fortresses.
Introduction: The Silence Before That Final Commit
Congratulations. Your new feature is complete, tests are passing, and your API is running like clockwork. Performance is great, functionality is just as you intended. You're just about to hit the 'deploy' button. But have you stopped for a moment to think? How secure is this API? For most developers, security feels more like a chore to be "handled later" than a feature. This very mindset is the starting point for million-dollar data breaches, loss of reputation, and the erosion of user trust.
In this guide, we're setting aside the abstract items from OWASP's intimidating lists and breaking down the 5 most critical API security vulnerabilities with real-life scenarios and code examples that every developer can understand. Our goal isn't to turn you into a cybersecurity expert, but to enable you to look at every line of code you write through a "security lens."
1. Broken Object Level Authorization (BOLA)
What is it? This is the most common and most dangerous API vulnerability. In its simplest form, it's when a user can access data belonging to another user that they should not have access to. The API asks "who are you?" (Authentication) but forgets to ask "are you allowed to see this data?" (Authorization).
Attack Scenario
Consider an endpoint on an e-commerce site where you view your orders: GET /api/orders/123. This request fetches the details for order ID 123. An attacker captures this request and simply changes the ID: GET /api/orders/124. If the API has a BOLA vulnerability, the server will execute this request without question, serving up someone else's order details (address, products purchased, amount paid) on a silver platter.
Vulnerable Code Example (Node.js/Express)
// WRONG: Just being logged in is considered sufficient.
router.get('/orders/:orderId', isAuthenticated, async (req, res) => {
try {
const order = await Order.findById(req.params.orderId);
if (!order) {
return res.status(404).send('Order not found.');
}
res.json(order); // NO CHECK! Does this order belong to this user?
} catch (err) {
res.status(500).send('Server error.');
}
});
Secure Code Example
// CORRECT: The user's identity is compared with the data's ownership.
router.get('/orders/:orderId', isAuthenticated, async (req, res) => {
try {
const order = await Order.findById(req.params.orderId);
// The req.user object is added by the isAuthenticated middleware after decoding the JWT.
if (!order || order.userId.toString() !== req.user.id) {
// Returning 404 for both non-existent orders and orders belonging to others
// is more secure as it doesn't leak information to the attacker.
return res.status(404).send('Order not found.');
}
res.json(order);
} catch (err) {
res.status(500).send('Server error.');
}
});
How to Prevent It:
- On every endpoint, check if the user trying to access the data is the owner of that data.
- Consider centralizing these checks in an authorization layer (e.g., middleware).
- Use hard-to-guess UUIDs instead of predictable IDs (1, 2, 3...). This doesn't fix the vulnerability, but it makes the attacker's job harder.
2. Broken Authentication
What is it? Flaws in the processes of verifying users' identities. Weak password policies, leaked session IDs, and, most importantly, the incorrect implementation of JWT (JSON Web Token) fall into this category.
Attack Scenario: JWT Algorithm Manipulation
An attacker obtains a JWT signed with an algorithm like HS256. They modify the token's payload (user ID, role, etc.) as they wish. Then, they change the alg (algorithm) parameter in the header to none and delete the signature. If the JWT library on the server is not configured to handle this, it might interpret this as "no algorithm, so no signature check needed" and accept the fake token as valid. Result: The attacker can become any user they want, even an admin.
Secure Code Example (JWT Verification)
// CORRECT: Explicitly specifying which algorithms are acceptable.
const jwt = require('jsonwebtoken');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'] // Only accept this algorithm!
});
req.user = decoded;
} catch (err) {
// Token is invalid (signature error, expired, etc.)
return res.status(401).send('Invalid Token.');
}
How to Prevent It:
- Limit login attempts to prevent brute-force attacks (rate limiting).
- Enforce strong password policies and ensure your password reset mechanisms are secure.
- Make your JWT secret key long, complex, and unpredictable. Never leave it hard-coded in the source code; store it as an environment variable.
- Ensure your JWT library prevents vulnerabilities like
alg: noneand always specify the accepted algorithms.
3. Broken Object Property Level Authorization
What is it? This vulnerability appears in two forms. Excessive Data Exposure: The API sends all properties of an object, including sensitive ones, to the client, leaving the filtering to the client-side. Mass Assignment: Writing data from the client directly to a database object without filtering.
Attack Scenario
Excessive Data Exposure: The GET /api/users/me endpoint fetches the user object from the database and returns it as JSON. Although the frontend only displays the name and email fields, an attacker who opens the browser's network tab can see sensitive information in the response, such as "userRole": "customer" or "passwordHash": "...".
Mass Assignment: The endpoint used to update a user profile, PATCH /api/users/me, applies the incoming JSON body directly to the database model. An attacker adds a field to the request that is not on the form: { "email": "[email protected]", "isAdmin": true }. If the API doesn't filter this request, the attacker makes themselves an admin.
Secure Code Example (Mass Assignment Prevention)
// WRONG: Trying to update with the entire request body
router.patch('/users/me', isAuthenticated, async (req, res) => {
const user = await User.findByIdAndUpdate(req.user.id, req.body, { new: true });
res.json(user); // Dangerous! The user can make themselves an admin.
});
// CORRECT: Only updating allowed fields
router.patch('/users/me', isAuthenticated, async (req, res) => {
const updates = {};
if (req.body.firstName) updates.firstName = req.body.firstName;
if (req.body.lastName) updates.lastName = req.body.lastName;
// Fields like 'isAdmin' or 'role' are never taken from the client.
const user = await User.findByIdAndUpdate(req.user.id, { $set: updates }, { new: true });
// Use a DTO (Data Transfer Object) to return only safe data
res.json({ id: user.id, firstName: user.firstName, lastName: user.lastName });
});
How to Prevent It:
- Never return the entire database object in your API responses. Create DTOs or ViewModels that contain only the fields the client needs.
- Don't blindly write incoming data to the database. Only update fields that the user is allowed to change (using a "whitelist").
4. Unrestricted Resource Consumption
What is it? The API does not place any limits on how much or how often a client can consume resources (CPU, memory, bandwidth). This opens the door to Denial-of-Service (DoS) attacks.
Attack Scenario
An attacker sends thousands of requests per second to a GET /api/products endpoint that lacks pagination. Each request queries thousands of records in the database, consuming the server's CPU and memory, making it unable to serve other legitimate users. Or, even a single request can be enough to crash the server if there are millions of products in the database.
Secure Code Example (Rate Limiting and Pagination)
const rateLimit = require('express-rate-limit');
// Rate Limiter Middleware
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Each IP can make 100 requests in this window
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api', apiLimiter); // Apply to all /api endpoints
// Pagination
router.get('/products', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const products = await Product.find().skip(skip).limit(limit);
res.json(products);
});
How to Prevent It:
- Apply a reasonable rate limit to all your API endpoints.
- Always use server-side pagination on endpoints that return lists, and set a maximum limit value.
- Impose a limit on request payload size.
- Set timeouts to terminate long-running requests.
5. Broken Function Level Authorization
What is it? Similar to BOLA, but it involves unauthorized access to functions (i.e., endpoints) rather than objects. It's the improper implementation of distinctions between user roles (e.g., user, editor, admin) at the API level.
Attack Scenario
Your API has both regular users and admins. A regular user can view their own profile with the GET /api/users/me endpoint. The attacker tries to guess the structure of your API and attempts to call endpoints like GET /api/admin/dashboard or DELETE /api/users/456. If these endpoints only check for authentication but not for role, a regular user can perform actions that require admin privileges.
Secure Code Example (Role Check Middleware)
// A middleware to check for roles
const adminOnly = (req, res, next) => {
// req.user should have been added by a previous authentication middleware
if (req.user && req.user.role === 'admin') {
next(); // User is an admin, proceed
} else {
res.status(403).send('Access denied. You do not have permission to perform this action.');
}
};
// The sensitive endpoint is protected by this middleware
router.delete('/users/:userId', isAuthenticated, adminOnly, async (req, res) => {
// ... user deletion logic ...
});
How to Prevent It:
- Adopt a "deny-by-default" principle. Access should be denied unless explicitly granted for an endpoint.
- Separate administrative functions from regular user APIs, possibly grouping them in a different API set.
- Clearly define user roles and which functions these roles can access, and manage these controls from a central location.
Conclusion: Security Is a Journey, Not a Destination
While these five vulnerabilities are just the tip of the iceberg, they are the most frequently encountered and most devastating. Remember, API security is not a one-time checklist. It is a mindset that you must integrate into every step of your development lifecycle. Asking "How can this be exploited?" from the moment you start coding is the most important step that will transform you from an average developer into a responsible and trustworthy engineer.
