API Security: OWASP API Top 10
APIs are the primary attack surface of modern applications. Here are defenses against the most critical vulnerabilities.
1. Broken Object Level Authorization (BOLA)
// VULNERABLE: No ownership check
app.get('/api/orders/:id', async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order); // Any user can access any order!
});
// SECURE: Verify the user owns the resource
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id, // Ownership check
});
if (!order) return res.status(404).json({ error: 'Not found' });
res.json(order);
});
2. Broken Authentication
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 attempts per 15 minutes
message: 'Too many login attempts, try again later',
skipSuccessfulRequests: true,
});
app.post('/api/login', loginLimiter, async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '15m' });
res.json({ token });
});
3. Mass Assignment Prevention
// VULNERABLE: User can set isAdmin: true
app.put('/api/users/:id', async (req, res) => {
await User.findByIdAndUpdate(req.params.id, req.body); // DANGEROUS
});
// SECURE: Whitelist allowed fields
const ALLOWED_FIELDS = ['name', 'email', 'bio', 'avatar'];
app.put('/api/users/:id', authenticate, async (req, res) => {
const updates = Object.keys(req.body)
.filter(key => ALLOWED_FIELDS.includes(key))
.reduce((obj, key) => ({ ...obj, [key]: req.body[key] }), {});
await User.findByIdAndUpdate(req.params.id, { $set: updates });
res.json({ success: true });
});
4. SQL Injection Prevention
// VULNERABLE
const result = await db.query(`SELECT * FROM users WHERE name = '${name}'`);
// SECURE: Parameterized queries
const result = await db.query('SELECT * FROM users WHERE name = $1', [name]);
// SECURE: Validate input with Zod
import { z } from 'zod';
const searchSchema = z.object({
q: z.string().max(100).regex(/^[a-zA-Z0-9 ]+$/),
limit: z.number().int().min(1).max(100).default(20),
});
app.get('/api/search', (req, res) => {
const params = searchSchema.parse(req.query); // Throws if invalid
// Use params safely
});
5. Security Headers
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
referrerPolicy: { policy: 'same-origin' },
}));
app.use(cors({
origin: ['https://myapp.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
app.disable('x-powered-by');
6. Rate Limiting Per User
import { RateLimiterRedis } from 'rate-limiter-flexible';
const apiLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 100, // requests
duration: 60, // per 60 seconds per user
blockDuration: 60,
});
const rateLimitMiddleware = async (req: Request, res: Response, next: NextFunction) => {
try {
await apiLimiter.consume(req.user?.id || req.ip);
next();
} catch {
res.status(429).json({ error: 'Too many requests', retryAfter: 60 });
}
};
7. JWT Best Practices
// Short-lived access tokens + refresh tokens
const accessToken = jwt.sign(
{ id: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m', issuer: 'myapi.com', algorithm: 'HS256' }
);
const refreshToken = jwt.sign(
{ id: user.id, type: 'refresh' },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Store refresh token hash in DB for revocation
await db.refreshTokens.create({
userId: user.id,
tokenHash: crypto.createHash('sha256').update(refreshToken).digest('hex'),
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
});
Security Testing Checklist
- Test BOLA: Can user A access user B resources?
- Test mass assignment: Can user set admin fields?
- Test injection: SQL, NoSQL, command injection
- Test rate limiting: Can you brute-force login?
- Check security headers: securityheaders.com
- Test JWT: expired, wrong signature, algorithm confusion
- Run OWASP ZAP or Burp Suite automated scan
Summary
API security is layered defense:
- Always check object ownership (BOLA)
- Rate limit authentication endpoints
- Whitelist allowed fields for updates
- Use parameterized queries exclusively
- Set security headers with Helmet
- Use short-lived JWTs with refresh tokens
- Validate all input with a schema library