Meta Description: Protect your REST API from the OWASP Top 10 vulnerabilities. Learn practical defenses against injection, broken auth, excessive data exposure, and more.
Keywords: api security, owasp top 10, rest api security, api vulnerabilities, broken authentication, injection attacks
Word Count: ~2,500 words
The OWASP API Security Top 10 lists the most critical API vulnerabilities. These aren't theoretical risks—they're the vulnerabilities attackers exploit in real systems.
Here's how to defend against each one.
1. Broken Object Level Authorization (BOLA)
The most common API vulnerability. Users access objects they don't own.
Vulnerable:
app.get('/v1/pets/:id', async (req, res) => {
const pet = await db.pets.findById(req.params.id);
res.json(pet); // Returns any pet, regardless of ownership
});
Attacker changes the ID to access other users' pets:
GET /v1/pets/123 ← Their pet
GET /v1/pets/456 ← Someone else's pet (unauthorized)
Fixed:
app.get('/v1/pets/:id', async (req, res) => {
const pet = await db.pets.findById(req.params.id);
if (!pet) {
return res.status(404).json({ error: 'Pet not found' });
}
// Check ownership
if (pet.ownerId !== req.auth.userId) {
return res.status(403).json({ error: 'Access denied' });
}
res.json(pet);
});
Rule: Always verify the authenticated user owns or has permission to access the requested object.
2. Broken Authentication
Weak authentication mechanisms allow attackers to impersonate users.
Common issues: - Weak passwords allowed - No rate limiting on login - Tokens never expire - Tokens not invalidated on logout
Fixes:
Rate limit authentication endpoints:
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts per window
message: { error: 'Too many login attempts' }
});
app.post('/v1/auth/login', loginLimiter, async (req, res) => {
// Handle login
});
Use short-lived tokens:
const token = jwt.sign(
{ userId: user.id, scopes: user.scopes },
JWT_SECRET,
{ expiresIn: '1h' } // Expire after 1 hour
);
Invalidate tokens on logout:
// Token blocklist
const blocklist = new Set();
app.post('/v1/auth/logout', (req, res) => {
blocklist.add(req.token.jti); // Add token ID to blocklist
res.status(204).send();
});
// Check blocklist on every request
function validateToken(token) {
const decoded = jwt.verify(token, JWT_SECRET);
if (blocklist.has(decoded.jti)) {
throw new Error('Token revoked');
}
return decoded;
}
3. Broken Object Property Level Authorization
Users can read or modify object properties they shouldn't access.
Vulnerable (mass assignment):
app.put('/v1/users/:id', async (req, res) => {
// Attacker sends: { "role": "admin", "name": "John" }
const user = await db.users.update(req.params.id, req.body);
res.json(user);
});
Attacker escalates their own privileges by sending role: admin.
Fixed (allowlist fields):
app.put('/v1/users/:id', async (req, res) => {
// Only allow specific fields
const allowedFields = ['name', 'email', 'bio', 'avatar'];
const updateData = {};
allowedFields.forEach(field => {
if (req.body[field] !== undefined) {
updateData[field] = req.body[field];
}
});
const user = await db.users.update(req.params.id, updateData);
res.json(user);
});
Also fix response filtering:
function sanitizeUser(user) {
// Remove sensitive fields from response
const { passwordHash, internalNotes, adminFlags, ...safeUser } = user;
return safeUser;
}
app.get('/v1/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(sanitizeUser(user));
});
4. Unrestricted Resource Consumption
No limits on request size, rate, or resource usage.
Vulnerable:
app.post('/v1/pets/search', async (req, res) => {
// No limit on results
const pets = await db.pets.findMany(req.body.filters);
res.json(pets); // Could return millions of records
});
Fixed:
app.post('/v1/pets/search', async (req, res) => {
const limit = Math.min(req.body.limit || 20, 100); // Max 100 results
const pets = await db.pets.findMany({
...req.body.filters,
limit: limit
});
res.json({ data: pets, limit });
});
Add request size limits:
app.use(express.json({ limit: '1mb' })); // Max 1MB request body
Add query complexity limits (for GraphQL):
const depthLimit = require('graphql-depth-limit');
const { createComplexityLimitRule } = require('graphql-validation-complexity');
app.use('/graphql', graphqlHTTP({
schema,
validationRules: [
depthLimit(5), // Max 5 levels deep
createComplexityLimitRule(1000) // Max complexity score
]
}));
5. Broken Function Level Authorization
Users access admin or privileged functions.
Vulnerable:
app.delete('/v1/admin/users/:id', async (req, res) => {
// No admin check!
await db.users.delete(req.params.id);
res.status(204).send();
});
Fixed:
function requireAdmin(req, res, next) {
if (!req.auth.scopes.includes('admin:all')) {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
app.delete('/v1/admin/users/:id', requireAdmin, async (req, res) => {
await db.users.delete(req.params.id);
res.status(204).send();
});
Don't rely on obscurity:
// BAD: Hiding admin endpoints
app.delete('/v1/xk9m2p/users/:id', ...); // Security through obscurity
// GOOD: Proper authorization
app.delete('/v1/admin/users/:id', requireAdmin, ...);
6. Unrestricted Access to Sensitive Business Flows
Attackers abuse legitimate API flows at scale.
Example: Automated pet adoption applications
// Vulnerable: No rate limiting on applications
app.post('/v1/pets/:id/applications', async (req, res) => {
const application = await createApplication(req.body);
res.status(201).json(application);
});
Attacker submits thousands of applications to monopolize pets.
Fixed:
// Rate limit per user
const applicationLimiter = rateLimit({
windowMs: 24 * 60 * 60 * 1000, // 24 hours
max: 5, // 5 applications per day
keyGenerator: (req) => req.auth.userId
});
// Limit pending applications
app.post('/v1/pets/:id/applications', applicationLimiter, async (req, res) => {
const pendingCount = await db.applications.count({
userId: req.auth.userId,
status: 'PENDING'
});
if (pendingCount >= 3) {
return res.status(429).json({
error: 'Too many pending applications',
detail: 'You can have at most 3 pending applications'
});
}
const application = await createApplication(req.body);
res.status(201).json(application);
});
7. Server-Side Request Forgery (SSRF)
API fetches URLs provided by users, allowing access to internal systems.
Vulnerable:
app.post('/v1/pets/import', async (req, res) => {
// Attacker sends: { "imageUrl": "http://169.254.169.254/latest/meta-data/" }
const image = await fetch(req.body.imageUrl);
// Fetches AWS metadata, internal services, etc.
});
Fixed:
const { URL } = require('url');
function isSafeUrl(urlString) {
try {
const url = new URL(urlString);
// Only allow HTTPS
if (url.protocol !== 'https:') return false;
// Block private IP ranges
const hostname = url.hostname;
const privateRanges = [
/^localhost$/,
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
/^169\.254\./ // AWS metadata
];
if (privateRanges.some(range => range.test(hostname))) {
return false;
}
return true;
} catch {
return false;
}
}
app.post('/v1/pets/import', async (req, res) => {
if (!isSafeUrl(req.body.imageUrl)) {
return res.status(400).json({ error: 'Invalid image URL' });
}
const image = await fetch(req.body.imageUrl);
// Safe to proceed
});
8. Security Misconfiguration
Default settings, verbose errors, and missing security headers.
Add security headers:
const helmet = require('helmet');
app.use(helmet());
// Or manually:
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Content-Security-Policy', "default-src 'none'");
next();
});
Don't expose stack traces:
// BAD
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message,
stack: err.stack // Exposes internal details
});
});
// GOOD
app.use((err, req, res, next) => {
console.error(err); // Log internally
res.status(500).json({
type: 'https://petstoreapi.com/errors/internal-error',
title: 'Internal Server Error',
status: 500
});
});
Disable unnecessary HTTP methods:
app.use((req, res, next) => {
const allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
if (!allowedMethods.includes(req.method)) {
return res.status(405).json({ error: 'Method not allowed' });
}
next();
});
9. Improper Inventory Management
Undocumented or forgotten API versions and endpoints.
Maintain an API inventory:
# api-inventory.yaml
versions:
v1:
status: deprecated
sunset: 2027-01-01
endpoints: 45
v2:
status: current
endpoints: 52
environments:
production: https://api.petstoreapi.com
staging: https://staging.api.petstoreapi.com
development: http://localhost:3000
Add deprecation headers:
app.use('/v1', (req, res, next) => {
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', 'Sat, 01 Jan 2027 00:00:00 GMT');
res.setHeader('Link', '</v2>; rel="successor-version"');
next();
});
Disable debug endpoints in production:
if (process.env.NODE_ENV !== 'production') {
app.get('/debug/config', (req, res) => {
res.json(config);
});
}
10. Unsafe Consumption of APIs
Trusting third-party API responses without validation.
Vulnerable:
async function getBreedInfo(breed) {
const response = await fetch(`https://dog-api.example.com/breeds/${breed}`);
const data = await response.json();
// Trusting external data directly
return {
name: data.name,
description: data.description,
imageUrl: data.imageUrl // Could be malicious URL
};
}
Fixed:
async function getBreedInfo(breed) {
const response = await fetch(`https://dog-api.example.com/breeds/${breed}`);
if (!response.ok) {
throw new Error(`External API error: ${response.status}`);
}
const data = await response.json();
// Validate and sanitize external data
return {
name: sanitizeString(data.name, { maxLength: 100 }),
description: sanitizeString(data.description, { maxLength: 1000 }),
imageUrl: isSafeUrl(data.imageUrl) ? data.imageUrl : null
};
}
function sanitizeString(value, options = {}) {
if (typeof value !== 'string') return '';
let sanitized = value.trim();
if (options.maxLength) {
sanitized = sanitized.substring(0, options.maxLength);
}
return sanitized;
}
Security Checklist
Use this checklist for every API endpoint:
Authorization: - [ ] Verify user owns the requested resource (BOLA) - [ ] Check function-level permissions - [ ] Allowlist fields for updates (mass assignment) - [ ] Filter sensitive fields from responses
Authentication: - [ ] Rate limit auth endpoints - [ ] Use short-lived tokens - [ ] Invalidate tokens on logout
Input Validation: - [ ] Validate all input types and formats - [ ] Limit request body size - [ ] Sanitize strings for injection
Rate Limiting: - [ ] Limit requests per user - [ ] Limit results per request - [ ] Limit business flow operations
Configuration: - [ ] Add security headers - [ ] Hide stack traces in production - [ ] Disable debug endpoints in production
Testing Your API Security
Use these tools to test your defenses:
OWASP ZAP: Automated security scanning Burp Suite: Manual security testing Postman: API testing with security checks Apidog: API testing with security validation
Run security tests in your CI/CD pipeline:
# GitHub Actions
- name: Security Scan
run: |
docker run -t owasp/zap2docker-stable zap-api-scan.py \
-t https://staging.api.petstoreapi.com/openapi.json \
-f openapi
Catch vulnerabilities before they reach production.
Meta Tags: - Title: How to Secure REST APIs Against the OWASP Top 10 - Description: Protect your REST API from the OWASP Top 10 vulnerabilities. Learn practical defenses against injection, broken auth, excessive data exposure, and more. - Keywords: api security, owasp top 10, rest api security, api vulnerabilities