Every week, another company announces a data breach. The attackers aren't using zero-days or sophisticated malware—they're exploiting the same API vulnerabilities that have existed for years. In 2026, API security is still an afterthought for most teams, and attackers know it.
I spent the last six months analyzing real-world API breaches. Here's what's actually hitting production systems.
The OWASP API Top 10 Hasn't Changed (And Neither Has Industry Response) #
The OWASP API Security Top 10 looks almost identical to 2019:
- Broken Object Level Authorization (BOLA)
- Broken Authentication
- Broken Object Property Level Authorization
- Unrestricted Resource Consumption
- Broken Function Level Authorization
- Mass Assignment
- Security Misconfiguration
- Injection
- Improper Inventory Management
- Unsafe Consumption of APIs
These aren't theoretical. Every single one has been exploited in major breaches in the past 18 months.
Attack #1: BOLA — The Silent Data Extractor #
Broken Object Level Authorization is responsible for more data breaches than any other vulnerability. The pattern is always the same: an API endpoint exposes an object ID, and the API doesn't verify if the authenticated user actually owns that object.
Real Example (Simplified from an Actual Breach)
// VULNERABLE API
app.get('/api/orders/:orderId', authMiddleware, async (req, res) => {
const order = await Order.findById(req.params.orderId);
res.json(order); // No ownership check!
});
// ATTACK: Iterate through order IDs
// curl https://api.example.com/api/orders/1
// curl https://api.example.com/api/orders/2
// curl https://api.example.com/api/orders/3
// ... extracted 50,000 customer records
How to Fix It
// SECURE API
app.get('/api/orders/:orderId', authMiddleware, async (req, res) => {
// Explicit ownership check
const order = await Order.findOne({
_id: req.params.orderId,
userId: req.user.id // Always filter by owner
});
if (!order) {
return res.status(404).json({
error: 'Order not found'
});
}
res.json(order);
});
The critical lesson: always verify ownership, not just authentication. Authenticated doesn't mean authorized for that specific object.
Attack #2: Broken Authentication — The JWT Mistakes #
JSON Web Tokens are everywhere, and they're frequently implemented wrong. Here's a sampling of real JWT vulnerabilities I've found in production APIs.
Vulnerability: Algorithm Confusion
// VULNERABLE: Server accepts any algorithm
const decoded = jwt.verify(token, publicKey, {
algorithms: ['HS256', 'RS256'] // DON'T DO THIS
});
// ATTACK: Change RS256 to HS256 and sign with the public key
// Since the server uses the SAME key for both symmetric and asymmetric,
// the attacker can forge tokens by signing HS256 with the RSA public key
js
// SECURE: Explicit algorithm allowlist
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'] // Only allow the intended algorithm
});
Vulnerability: None Algorithm
// VULNERABLE: Some libraries accept 'none' algorithm
jwt.verify(token, '', { algorithms: ['none'] });
// Produces: {"alg":"none","typ":"JWT"}
// becomes: eyJhbGciOiJub25lIiwidHlwIjoiand0In0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
js
// SECURE: Explicitly reject 'none'
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
alloweds: { algorithms: ['HS256', 'RS256'] } // Reject none
});
// Or use a library that defaults to rejecting 'none'
const decoded = jwt.verify(token, publicKey);
Attack #3: Unrestricted Resource Consumption — The Denial of Wallet #
This one is underrated and increasingly common. Attackers don't need to crash your service—they just need to make it expensive to run.
Real-World Scenario
@app.get("/api/search")
def search(q: str):
results = db.query(f"SELECT * FROM products WHERE name LIKE '%{q}%'")
return results # Returns ALL matches, attacker controls result set size
python
@app.get("/api/search")
def search(
q: str,
limit: int = Query(default=20, ge=1, le=100), # Max 100
offset: int = Query(default=0, ge=0, le=10000) # Max offset
):
results = db.query(
"SELECT * FROM products WHERE name LIKE %s LIMIT %s OFFSET %s",
(f"%{q}%", limit, offset)
)
if len(q) > 100:
raise HTTPException(400, "Query too long")
return results
The Password Reset DoS
@app.post("/api/password-reset")
def request_reset(email: str):
user = db.find_user(email)
if user:
send_email(user.email, generate_token()) # No throttle
return {"message": "If email exists, reset was sent"}
python
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@app.post("/api/password-reset")
@limiter.limit("3/hour") # 3 attempts per hour per IP
def request_reset(email: str, request: Request):
user = db.find_user(email)
if user:
send_email(user.email, generate_token())
return {"message": "If email exists, reset was sent"}
Attack #4: Mass Assignment — The Hidden Parameter #
// VULNERABLE: Trusting client input blindly
app.post('/api/profile', authMiddleware, async (req, res) => {
await User.updateOne(
{ _id: req.user.id },
{ $set: req.body } // Client can set ANY field
);
});
// ATTACK: Send this request
// POST /api/profile
// {"name": "John", "role": "admin", "isVerified": true, "creditLimit": 1000000}
//
// Suddenly the regular user is an admin with unlimited credit
// SECURE: Explicit field allowlist
app.post('/api/profile', authMiddleware, async (req, res) => {
const allowedFields = ['name', 'bio', 'avatarUrl', 'timezone'];
const updates = {};
for (const field of allowedFields) {
if (field in req.body) {
updates[field] = req.body[field];
}
}
await User.updateOne(
{ _id: req.user.id },
{ $set: updates }
);
res.json({ success: true });
});
Attack #5: The Unprotected Admin Endpoints #
@app.post("/api/admin/users/delete")
def delete_user(user_id: str):
db.delete_user(user_id)
return {"success": True}
python
from functools import wraps
def require_admin(f):
@wraps(f)
async def decorated(*args, **kwargs):
if not request.user or request.user.role != 'admin':
return {"error": "Forbidden"}, 403
return await f(*args, **kwargs)
return decorated
@app.post("/api/admin/users/delete")
@require_admin
@require_auth
def delete_user(user_id: str):
db.delete_user(user_id)
return {"success": True}
The Testing Framework You Should Be Using #
import httpx
import pytest
class TestAPISecurity:
"""API Security Test Suite - Run these against staging before every deploy"""
def test_bola_object_level_access(self):
"""Test that users can't access other users' resources"""
user1_token = self.get_token("user1@example.com")
user2_resource = self.create_resource("user2@example.com")
response = httpx.get(
f"{BASE_URL}/api/resources/{user2_resource.id}",
headers={"Authorization": f"Bearer {user1_token}"}
)
assert response.status_code == 403, "BOLA vulnerability: User can access others' resources!"
def test_jwt_algorithm_confusion(self):
"""Test JWT algorithm confusion attack"""
token = self.get_valid_token()
header_b64 = base64.b64encode(b'{"alg":"HS256","typ":"JWT"}').decode()
tampered = f"{header_b64}.{token.split('.')[1]}.{fake_signature}"
response = httpx.get(
f"{BASE_URL}/api/protected",
headers={"Authorization": f"Bearer {tampered}"}
)
assert response.status_code == 401, "JWT algorithm confusion succeeded!"
def test_mass_assignment_protection(self):
"""Test that users can't set admin fields"""
user_token = self.get_token("regular@example.com")
response = httpx.post(
f"{BASE_URL}/api/profile",
headers={"Authorization": f"Bearer {user_token}"},
json={"name": "Test", "role": "admin", "isVerified": True}
)
profile = self.get_profile("regular@example.com")
assert profile["role"] != "admin", "Mass assignment vulnerability!"
def test_rate_limiting(self):
"""Test that rate limiting prevents abuse"""
for _ in range(100):
response = httpx.post(
f"{BASE_URL}/api/password-reset",
json={"email": "test@example.com"}
)
response = httpx.post(
f"{BASE_URL}/api/password-reset",
json={"email": "test@example.com"}
)
assert response.status_code == 429, "Rate limiting not enforced!"
The Security Checklist Before Every Deploy #
□ Object-level authorization tested for every endpoint
□ All JWT implementations use explicit algorithm allowlists
□ Rate limiting on all public endpoints
□ Pagination limits on all list endpoints
□ Mass assignment protection via field allowlists
□ Admin endpoints protected by role checks
□ No sensitive data in URL parameters (tokens, IDs)
□ SQL injection protection (parameterized queries)
□ No stack traces or internal errors in responses
□ CORS properly configured
□ Security headers present (CSP, X-Frame-Options, etc.)
The Harsh Reality #
API security in 2026 is still years behind application security. Most teams have:
- No API security testing in CI/CD
- No API inventory (how many endpoints do you have? Do you even know?)
- No rate limiting on 80% of endpoints
- Authentication without authorization checks
The attackers know this. They're scanning for these vulnerabilities at scale, automated, 24/7.
The good news: fixing these isn't hard. It just requires making security testing a first-class citizen in your development process.
What's your biggest API security challenge? Found any interesting vulnerabilities in the wild? Let's discuss.
Further reading: OWASP API Security Top 10 — still the best starting point for API security fundamentals.