API Security
APIs form the backbone of modern applications, but their security is often overlooked. The OWASP API Security Top 10 highlights critical vulnerabilities that affect both REST and GraphQL APIs. Understanding these risks is essential for building secure microservices and preventing data breaches.
OWASP API Security Top 10
The OWASP API Security Top 10 identifies the most critical risks:
- Broken Object Level Authorization (BOLA)
- Broken Authentication
- Broken Object Property Level Authorization
- Unrestricted Resource Consumption
- Broken Function Level Authorization
- Unrestricted Access to Sensitive Business Flows
- Server-Side Request Forgery
- Security Misconfiguration
- Improper Inventory Management
- Unsafe Consumption of APIs
Broken Object Level Authorization (BOLA)
BOLA (formerly IDOR) is the most critical API vulnerability. It occurs when APIs expose object IDs without validating that the requesting user has permission to access them.
Vulnerable REST API
// Vulnerable: No authorization check
app.get('/api/users/:id/orders', async (req, res) => {
const { id } = req.params;
// DANGEROUS: Just fetches orders without checking ownership
const orders = await db.query(
'SELECT * FROM orders WHERE user_id = ?',
[id]
);
res.json(orders);
});
// Attack: Change user ID to access other users' orders
// GET /api/users/12345/orders (attacker's ID: 99999)
// Returns another user's order historySecure Implementation
// Secure: Verify ownership
app.get('/api/users/:id/orders', authenticate, async (req, res) => {
const requestedUserId = req.params.id;
const authenticatedUserId = req.user.id;
// Check if user can access this resource
if (requestedUserId !== authenticatedUserId && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
const orders = await db.query(
'SELECT * FROM orders WHERE user_id = ?',
[requestedUserId]
);
res.json(orders);
});
// Even better: Don't expose IDs at all
app.get('/api/my/orders', authenticate, async (req, res) => {
const orders = await db.query(
'SELECT * FROM orders WHERE user_id = ?',
[req.user.id] // Always use authenticated user's ID
);
res.json(orders);
});GraphQL-Specific Vulnerabilities
Introspection Exposure
GraphQL introspection reveals your entire API schema, including types, queries, mutations, and sometimes deprecated or hidden fields:
# Introspection query - reveals entire schema
query {
__schema {
types {
name
fields {
name
type { name }
}
}
}
}
# Response exposes everything:
# - adminDeleteUser mutation (hidden from docs)
# - internalNotes field on User type
# - deprecatedPassword field (oops!)Batching Attacks
# Batching attack - brute force in single request
query {
a1: login(email: "admin@example.com", password: "password1") { token }
a2: login(email: "admin@example.com", password: "password2") { token }
a3: login(email: "admin@example.com", password: "password3") { token }
# ... hundreds more attempts in one request
a500: login(email: "admin@example.com", password: "password500") { token }
}
# Rate limiting per-request won't stop thisNested Query DoS
# DoS via deeply nested query
query {
user(id: "1") {
friends {
friends {
friends {
friends {
friends {
# N+1 query explosion
# Each level multiplies database queries
name
}
}
}
}
}
}
}Mass Assignment
APIs that blindly accept all client-supplied data can allow attackers to modify fields they shouldn't have access to:
// Vulnerable: Accepts all fields from request body
app.put('/api/users/:id', authenticate, async (req, res) => {
const { id } = req.params;
// DANGEROUS: Spreads entire request body into update
await db.query('UPDATE users SET ? WHERE id = ?', [req.body, id]);
res.json({ success: true });
});
// Attack: Add isAdmin field to request
// PUT /api/users/123
// { "name": "hacker", "isAdmin": true, "role": "superuser" }Prevention: Allowlist Fields
// Secure: Explicitly allowlist fields
const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'avatar'];
app.put('/api/users/:id', authenticate, async (req, res) => {
const { id } = req.params;
// Only pick allowed fields
const updates = {};
for (const field of ALLOWED_UPDATE_FIELDS) {
if (req.body[field] !== undefined) {
updates[field] = req.body[field];
}
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({ error: 'No valid fields to update' });
}
await db.query('UPDATE users SET ? WHERE id = ?', [updates, id]);
res.json({ success: true });
});Rate Limiting and Resource Protection
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
// Per-user rate limiting
const apiLimiter = rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
keyGenerator: (req) => req.user?.id || req.ip,
message: { error: 'Too many requests' }
});
// Stricter limits for sensitive endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 attempts per hour
keyGenerator: (req) => req.body.email || req.ip,
message: { error: 'Too many login attempts' }
});
app.use('/api/', apiLimiter);
app.post('/api/auth/login', authLimiter);GraphQL Security
import { createServer } from '@graphql-yoga/node';
import { useDepthLimit } from '@graphql-yoga/plugin-depth-limit';
import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection';
const server = createServer({
plugins: [
// Disable introspection in production
process.env.NODE_ENV === 'production' && useDisableIntrospection(),
// Limit query depth to prevent DoS
useDepthLimit({ maxDepth: 5 }),
].filter(Boolean),
context: async ({ request }) => {
return {
user: await authenticateRequest(request),
};
},
});
// Resolver with authorization
const resolvers = {
Query: {
user: async (_, { id }, context) => {
// Always check authorization in resolvers
if (context.user.id !== id && !context.user.isAdmin) {
throw new GraphQLError('Not authorized');
}
return db.users.findById(id);
},
},
};Security Checklist
- Implement object-level authorization on every endpoint
- Use allowlists for mass assignment protection
- Apply rate limiting per user and per endpoint
- Disable GraphQL introspection in production
- Limit GraphQL query depth and complexity
- Validate all input with schemas (Zod, Joi)
- Use proper HTTP methods (GET for reads, POST for mutations)
- Return minimal data (avoid oversharing in responses)
- Maintain API inventory and deprecate old versions
- Log and monitor all API access patterns
Practice Challenges
View allMass Assignment
API accepts more fields than it should. isAdmin: true anyone?
API Versioning
v2 API is secure. But v1 is still running.
Method Override
DELETE blocked? Try X-HTTP-Method-Override.
API Key in URL
API key passed in query string. Hello, server logs.
Excessive Data Exposure
API returns entire user object. Including password hash.