7 Common API Security Vulnerabilities (and How to Fix Them)
Real API security vulnerabilities from the OWASP API Top 10 — with working code fixes, risk levels, and testing tools so you can protect your APIs today.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
I once found an authorization vulnerability in a production API during a code review that would have let any authenticated user read any other user's private messages just by changing a single number in the URL. The fix was four lines of code. The vulnerability had been live for eight months.
API security failures aren't usually sophisticated hacks. They're simple logic errors — forgetting to check ownership, not validating input, trusting client-provided IDs, misconfiguring CORS. The OWASP API Security Top 10 documents the most common patterns. Let's fix them.
1. Broken Object Level Authorization (BOLA/IDOR)
This is the vulnerability I described above. It's #1 on the OWASP API Top 10 for a reason: it's trivially easy to exploit and devastatingly common.
The vulnerability:
// VULNERABLE — no ownership check
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await db.query(
'SELECT * FROM invoices WHERE id = $1',
[req.params.id]
);
res.json(invoice.rows[0]);
});
// Any authenticated user can access any invoice by changing the ID
The fix:
// SECURE — always filter by authenticated user's ID
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await db.query(
'SELECT * FROM invoices WHERE id = $1 AND user_id = $2',
[req.params.id, req.user.userId]
);
if (!invoice.rows.length) {
// Return 404, not 403 — don't confirm the resource exists
return res.status(404).json({ error: 'Invoice not found' });
}
res.json(invoice.rows[0]);
});
The fix: always include the authenticated user's ID in your WHERE clause. Never assume that because a request is authenticated, the user is authorized to access that specific resource. For admin endpoints that legitimately need cross-user access, add explicit role checks.
2. Broken Authentication
Weak authentication implementations let attackers take over accounts. This covers everything from accepting weak passwords to improperly validating JWTs.
// VULNERABLE — common JWT verification mistakes
// Mistake 1: Not specifying allowed algorithms (algorithm confusion attack)
const payload = jwt.verify(token, secret); // Accepts any algorithm including 'none'
// Mistake 2: Not validating expiry explicitly (some libraries skip this)
const payload = jwt.verify(token, secret, { ignoreExpiration: true }); // Never do this
// Mistake 3: Using a weak or default secret
const SECRET = 'secret'; // Attackers can brute force this in minutes
// SECURE — proper JWT verification
import jwt from 'jsonwebtoken';
function verifyToken(token: string) {
return jwt.verify(token, process.env.JWT_SECRET!, {
algorithms: ['HS256'], // Explicitly allowlist algorithm
issuer: 'your-api-name', // Validate issuer claim
audience: 'your-client-name', // Validate audience claim
// expiry is checked by default — don't disable it
});
}
For login routes, add rate limiting and account lockout:
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per IP per window
message: { error: 'Too many login attempts. Try again in 15 minutes.' },
standardHeaders: true,
legacyHeaders: false,
});
app.post('/api/auth/login', loginLimiter, loginHandler);
Our JWT authentication tutorial covers the full secure implementation including refresh tokens and httpOnly cookies.
3. Broken Object Property Level Authorization (Excessive Data Exposure + Mass Assignment)
This covers two related problems: returning more data than clients need (exposure), and accepting more data from clients than you intend (mass assignment).
// VULNERABLE — returning full database object
app.get('/api/users/:id', authenticate, async (req, res) => {
const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
res.json(user.rows[0]);
// Sends: { id, email, password_hash, internal_notes, admin_flag, ... }
});
// SECURE — explicit field selection
app.get('/api/users/:id', authenticate, async (req, res) => {
const { rows } = await db.query(
'SELECT id, name, email, avatar_url, created_at FROM users WHERE id = $1',
[req.params.id]
);
if (!rows.length) return res.status(404).json({ error: 'Not found' });
res.json(rows[0]);
});
For mass assignment — where a client can set fields they shouldn't be able to:
// VULNERABLE — spreading request body directly
app.put('/api/users/:id', authenticate, async (req, res) => {
await db.query('UPDATE users SET $1 WHERE id = $2', [req.body, req.params.id]);
// Attacker can send: { "role": "admin", "is_verified": true }
});
// SECURE — allowlist only modifiable fields
app.put('/api/users/:id', authenticate, async (req, res) => {
const { name, bio, avatar_url } = req.body; // Only these fields are allowed
await db.query(
'UPDATE users SET name = $1, bio = $2, avatar_url = $3 WHERE id = $4 AND id = $5',
[name, bio, avatar_url, req.params.id, req.user.userId]
);
res.json({ success: true });
});
4. Unrestricted Resource Consumption (No Rate Limiting)
An API without rate limiting is an API that will be abused. This ranges from accidental abuse (a client with a bug making infinite requests) to deliberate DoS attacks.
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
// Global rate limiter
const globalLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100,
standardHeaders: true,
legacyHeaders: false,
// Use Redis for distributed rate limiting across multiple servers
store: new RedisStore({
sendCommand: (...args) => redisClient.sendCommand(args),
}),
});
// Strict limiter for expensive operations
const expensiveLimiter = rateLimit({
windowMs: 60 * 1000,
max: 10,
keyGenerator: (req) => req.user?.userId || req.ip, // Per-user, not per-IP
});
app.use('/api', globalLimiter);
app.post('/api/reports/generate', authenticate, expensiveLimiter, generateReport);
app.post('/api/ai/complete', authenticate, expensiveLimiter, aiComplete);
Also limit request body sizes:
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ limit: '100kb', extended: true }));
5. Broken Function Level Authorization
Some endpoints should only be accessible by certain roles. Missing role checks let regular users access admin functionality.
// Role-based access control middleware
function requireRole(...roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.delete(
'/api/admin/users/:id',
authenticate,
requireRole('admin', 'superadmin'),
deleteUser
);
app.get(
'/api/admin/audit-logs',
authenticate,
requireRole('admin'),
getAuditLogs
);
A common mistake: returning 403 for resources a user doesn't own. Return 404 instead — don't confirm that the resource exists to unauthorized callers.
6. CORS Misconfiguration
Cross-Origin Resource Sharing (CORS) misconfiguration is extremely common and ranges from overly permissive (allowing any origin) to broken (blocking legitimate requests).
// VULNERABLE — wildcard CORS allows any site to make authenticated requests
app.use(cors({ origin: '*', credentials: true }));
// Note: '*' with credentials: true is actually rejected by browsers,
// but developers often try this and then spend hours debugging CORS
// VULNERABLE — reflecting the Origin header without validation
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', req.headers.origin); // Any origin
res.set('Access-Control-Allow-Credentials', 'true');
next();
});
// SECURE — allowlist specific origins
const allowedOrigins = [
'https://yourapp.com',
'https://www.yourapp.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null,
].filter(Boolean);
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS policy violation: ${origin}`));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400, // Cache preflight for 24 hours
}));
7. SQL Injection and Input Validation
SQL injection doesn't appear in the latest OWASP API Top 10 as prominently as it used to because parameterized queries have become the default in most ORMs and query builders. But it still appears regularly in hand-rolled SQL.
// VULNERABLE — string interpolation in SQL
app.get('/api/search', async (req, res) => {
const query = `SELECT * FROM products WHERE name LIKE '%${req.query.q}%'`;
// Attacker sends: ?q=' OR '1'='1' --
const results = await db.query(query);
res.json(results.rows);
});
// SECURE — parameterized queries
app.get('/api/search', async (req, res) => {
const searchTerm = `%${req.query.q}%`;
const results = await db.query(
'SELECT id, name, price FROM products WHERE name ILIKE $1 LIMIT 50',
[searchTerm]
);
res.json(results.rows);
});
Add input validation with a library like Zod:
import { z } from 'zod';
const searchSchema = z.object({
q: z.string().min(1).max(100).trim(),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
app.get('/api/search', async (req, res) => {
const result = searchSchema.safeParse(req.query);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
const { q, page, limit } = result.data;
const offset = (page - 1) * limit;
const products = await db.query(
'SELECT id, name, price FROM products WHERE name ILIKE $1 LIMIT $2 OFFSET $3',
[`%${q}%`, limit, offset]
);
res.json({ products: products.rows, page, limit });
});
Vulnerability Reference Table
| Vulnerability | OWASP Rank | Risk Level | Primary Fix |
|---|---|---|---|
| Broken Object Level Authorization (BOLA) | #1 | Critical | Always filter by user ID in queries |
| Broken Authentication | #2 | Critical | Proper JWT validation + rate limiting |
| Broken Object Property Level Auth | #3 | High | Explicit field selection + allowlist writes |
| No Rate Limiting | #4 | High | Redis-backed rate limiting per user |
| Broken Function Level Authorization | #5 | High | Role-based middleware on all admin routes |
| CORS Misconfiguration | #8 | Medium–High | Origin allowlist, no wildcard with credentials |
| SQL Injection | #9 | Critical | Parameterized queries + input validation |
Testing Tools
- OWASP ZAP — Free automated scanner, great for finding obvious vulnerabilities
- Burp Suite Community — Manual testing proxy, excellent for BOLA and auth testing
- sqlmap — Automated SQL injection testing
- k6 — Load testing that doubles as rate limiting validation
- Postman — Manual API testing, useful for testing auth bypass scenarios
The fastest way to find authorization bugs in your own API: after logging in as User A, copy all the request IDs (invoice IDs, user IDs, document IDs) and try accessing them while authenticated as User B. Any 200 responses you get back are BOLA vulnerabilities.
Wrapping Up
These seven vulnerabilities account for the vast majority of real-world API security incidents. None of them require sophisticated attack tools to exploit — they're logical errors that show up in code reviews once you know what to look for.
The fixes are also not complex. Parameterized queries, ownership checks in WHERE clauses, rate limiting middleware, explicit field selection — these are patterns you can apply today. If you're building the authentication layer from scratch, our JWT authentication tutorial covers the secure patterns. For API design fundamentals, the API tutorial for beginners builds the right mental model from the ground up.
Security isn't a feature you add at the end. These checks need to be in every endpoint from the first line. The good news: once these patterns become habit, they're just how you write APIs.
Frequently Asked Questions
How do I test my API for security vulnerabilities?
Start with automated tools like OWASP ZAP or Burp Suite Community Edition for automated scanning. Combine this with manual testing using Postman or curl to test authentication bypass, parameter tampering, and rate limiting. Tools like sqlmap can test for SQL injection specifically.
What is the most critical API vulnerability in 2026?
Broken Object Level Authorization (BOLA/IDOR) remains the most common and impactful vulnerability according to OWASP. It's simple to exploit — just change an ID in a request — and devastatingly common in APIs that don't validate whether the authenticated user owns the requested resource.
Is HTTPS alone enough to secure an API?
No. HTTPS protects data in transit but does nothing to prevent authentication bypass, authorization failures, injection attacks, or excessive data exposure. HTTPS is a baseline requirement, not a security solution.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
How to Use Docker Compose for Local Dev (Node.js + PostgreSQL)
Set up a full local dev environment with Docker Compose, Node.js, PostgreSQL, and pgAdmin. Includes .env config, named volumes, healthchecks, and common error fixes.
5 GraphQL Resolver Best Practices (DataLoader, Error Handling)
Write efficient GraphQL resolvers that don't hammer your database. DataLoader N+1 fix, error handling patterns, auth in context, and resolver performance comparison.
7 API Authentication Methods Compared (API Key, JWT, OAuth, Basic)
Compare 7 API authentication methods — API keys, JWT, OAuth 2.0, Basic Auth, HMAC, mTLS, and session tokens — with code examples and a security tradeoff table.
How to Deploy a Node.js App on Kubernetes With Minikube (2026)
Step-by-step guide to deploying a Node.js application on Kubernetes using Minikube in 2026. Covers Dockerfile, Deployment YAML, Service config, and exposing your app.