Key Takeaways
- Always authenticate API requests — use OAuth 2.0 or API keys with rate limiting
- Validate and sanitize all input data before processing
- Implement rate limiting and throttling to prevent abuse
- Use HTTPS for all API communications — never send data over plain HTTP
- Follow the OWASP API Security Top 10 for comprehensive protection
OWASP API Security Top 10 (2023)
| # | Vulnerability | Description |
|---|---|---|
| API1 | Broken Object Level Authorization | APIs exposing endpoints that handle object IDs without proper authorization |
| API2 | Broken Authentication | Weak authentication mechanisms allowing attackers to compromise tokens |
| API3 | Broken Object Property Level Authorization | Exposing or allowing modification of sensitive object properties |
| API4 | Unrestricted Resource Consumption | No limits on API requests allowing DoS attacks |
| API5 | Broken Function Level Authorization | Complex access control policies leading to authorization flaws |
| API6 | Unrestricted Access to Sensitive Business Flows | Exposing business flows without considering abuse potential |
| API7 | Server Side Request Forgery (SSRF) | API fetches remote resources without validating user-supplied URLs |
| API8 | Security Misconfiguration | Insecure default configurations, verbose errors, unnecessary features |
| API9 | Improper Inventory Management | Exposed old API versions, undocumented endpoints |
| API10 | Unsafe Consumption of APIs | Trusting third-party APIs without proper validation |
Authentication
1. Use Strong Authentication
API Key Authentication (Simple)
# Pass API key in header (preferred)
curl -H "X-API-Key: your-api-key-here" https://api.example.com/data
# Never pass API keys in URL (logged in server logs!)
# BAD: https://api.example.com/data?api_key=secret
JWT (JSON Web Token) Authentication
# Request with JWT Bearer token
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." https://api.example.com/data
# Node.js - Verify JWT
const jwt = require('jsonwebtoken');
function verifyToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
# Python - Verify JWT
import jwt
from functools import wraps
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return {'error': 'Token missing'}, 401
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
current_user = User.query.get(data['user_id'])
except jwt.ExpiredSignatureError:
return {'error': 'Token expired'}, 401
except jwt.InvalidTokenError:
return {'error': 'Invalid token'}, 401
return f(current_user, *args, **kwargs)
return decorated
OAuth 2.0
# OAuth 2.0 Authorization Code Flow
# 1. Redirect user to authorization server
GET /authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
scope=read write&
state=random_csrf_token
# 2. Exchange code for access token
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=https://yourapp.com/callback&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
# 3. Use access token
curl -H "Authorization: Bearer ACCESS_TOKEN" https://api.example.com/resource
Authorization
2. Implement Proper Authorization
Object-Level Authorization (BOLA Prevention)
# BAD - No authorization check
@app.route('/api/users//orders')
def get_orders(user_id):
return Order.query.filter_by(user_id=user_id).all()
# GOOD - Verify user owns the resource
@app.route('/api/users//orders')
@token_required
def get_orders(current_user, user_id):
# Check if requesting their own data
if str(current_user.id) != user_id:
return {'error': 'Forbidden'}, 403
return Order.query.filter_by(user_id=user_id).all()
# Node.js example
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
const order = await Order.findById(req.params.orderId);
// Verify ownership
if (order.userId.toString() !== req.user.id) {
return res.status(403).json({ error: 'Forbidden' });
}
res.json(order);
});
Role-Based Access Control (RBAC)
# Python decorator for role checking
def require_role(*roles):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if current_user.role not in roles:
return {'error': 'Insufficient permissions'}, 403
return f(*args, **kwargs)
return decorated_function
return decorator
@app.route('/api/admin/users')
@token_required
@require_role('admin', 'superadmin')
def list_all_users():
return User.query.all()
# Node.js middleware
const requireRole = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
app.delete('/api/users/:id', authenticate, requireRole('admin'), deleteUser);
Rate Limiting
3. Implement Rate Limiting
Express.js Rate Limiting
const rateLimit = require('express-rate-limit');
// General rate limit
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: { error: 'Too many requests, please try again later' },
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
// Stricter limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 login attempts per hour
message: { error: 'Too many login attempts' },
});
app.use('/api/auth/login', authLimiter);
Python Flask Rate Limiting
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route("/api/resource")
@limiter.limit("10 per minute")
def get_resource():
return {"data": "resource"}
@app.route("/api/auth/login", methods=["POST"])
@limiter.limit("5 per hour")
def login():
# Login logic
pass
Nginx Rate Limiting
# nginx.conf
http {
# Define rate limit zone
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/m;
server {
# Apply to API endpoints
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend;
}
# Stricter limit for login
location /api/auth/login {
limit_req zone=login burst=5;
proxy_pass http://backend;
}
}
}
Input Validation
4. Validate All Input
Schema Validation (Node.js - Joi)
const Joi = require('joi');
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required(),
age: Joi.number().integer().min(18).max(120),
role: Joi.string().valid('user', 'admin').default('user')
});
app.post('/api/users', async (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
// Process validated data
});
Python (Pydantic / Marshmallow)
# Pydantic
from pydantic import BaseModel, EmailStr, validator
from typing import Optional
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
age: Optional[int] = None
@validator('username')
def username_alphanumeric(cls, v):
if not v.isalnum():
raise ValueError('must be alphanumeric')
if len(v) < 3 or len(v) > 30:
raise ValueError('must be 3-30 characters')
return v
@validator('password')
def password_strength(cls, v):
if len(v) < 8:
raise ValueError('must be at least 8 characters')
return v
@app.post("/api/users")
def create_user(user: UserCreate):
# Pydantic automatically validates
return {"user": user.dict()}
Prevent Injection Attacks
# SQL Injection - Use parameterized queries (see Database Security page)
# NoSQL Injection - Sanitize MongoDB queries
# BAD - Vulnerable to NoSQL injection
db.users.find({ username: req.body.username })
# Input: { "username": { "$gt": "" } } // Returns all users!
# GOOD - Type check and sanitize
const username = String(req.body.username).trim();
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
return res.status(400).json({ error: 'Invalid username' });
}
db.users.find({ username: username });
Security Headers & HTTPS
5. Use HTTPS and Security Headers
Express.js Security Headers (Helmet)
const helmet = require('helmet');
app.use(helmet());
// Or configure individually
app.use(helmet.contentSecurityPolicy());
app.use(helmet.crossOriginEmbedderPolicy());
app.use(helmet.crossOriginOpenerPolicy());
app.use(helmet.crossOriginResourcePolicy());
app.use(helmet.dnsPrefetchControl());
app.use(helmet.frameguard());
app.use(helmet.hsts());
app.use(helmet.ieNoOpen());
app.use(helmet.noSniff());
app.use(helmet.originAgentCluster());
app.use(helmet.permittedCrossDomainPolicies());
app.use(helmet.referrerPolicy());
app.use(helmet.xssFilter());
CORS Configuration
const cors = require('cors');
// BAD - Allow all origins
app.use(cors());
// GOOD - Whitelist specific origins
const corsOptions = {
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // 24 hours
};
app.use(cors(corsOptions));
Error Handling
6. Secure Error Handling
# BAD - Exposes internal details
{
"error": "Error: ECONNREFUSED 127.0.0.1:5432",
"stack": "Error: connect ECONNREFUSED\n at TCPConnectWrap..."
}
# GOOD - Generic error for clients
{
"error": "Internal server error",
"code": "INTERNAL_ERROR",
"requestId": "abc123" // For debugging via logs
}
// Node.js error handler
app.use((err, req, res, next) => {
// Log full error internally
console.error('Error:', {
message: err.message,
stack: err.stack,
requestId: req.id,
userId: req.user?.id
});
// Send sanitized response
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message,
requestId: req.id
});
});
Logging & Monitoring
7. Log Security Events
// Log security-relevant events
const securityLogger = {
authSuccess: (userId, ip) => {
logger.info('AUTH_SUCCESS', { userId, ip, timestamp: new Date() });
},
authFailure: (username, ip, reason) => {
logger.warn('AUTH_FAILURE', { username, ip, reason, timestamp: new Date() });
},
accessDenied: (userId, resource, ip) => {
logger.warn('ACCESS_DENIED', { userId, resource, ip, timestamp: new Date() });
},
suspiciousActivity: (userId, activity, ip) => {
logger.error('SUSPICIOUS_ACTIVITY', { userId, activity, ip, timestamp: new Date() });
}
};
// Usage
app.post('/api/auth/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findByUsername(username);
if (!user || !await user.verifyPassword(password)) {
securityLogger.authFailure(username, req.ip, 'Invalid credentials');
return res.status(401).json({ error: 'Invalid credentials' });
}
securityLogger.authSuccess(user.id, req.ip);
// Generate token...
});
Security Checklist
☐ HTTPS enforced for all endpoints
☐ Strong authentication implemented (OAuth2/JWT)
☐ Object-level authorization on all endpoints
☐ Role-based access control (RBAC)
☐ Rate limiting on all endpoints (stricter on auth)
☐ Input validation using schemas
☐ SQL/NoSQL injection prevention
☐ Security headers configured
☐ CORS properly restricted
☐ Error messages sanitized (no stack traces)
☐ Security events logged
☐ API versioning implemented
☐ Deprecated endpoints removed
☐ API documentation up to date
Related Resources
Test Your Knowledge
FixTheVuln Store
Get the OffSec OSWA Study Planner
Fillable PDF study planners with domain trackers, weekly schedules, and progress tracking. Available in Standard, ADHD-Friendly, Dark Mode, and 4-Format Bundle.
OffSec OSWA Planner60+ certifications available — from $5.99
CyberFolio
Choosing your next cert? Track them all in one place.
Build a shareable cybersecurity portfolio that highlights your certifications, projects, and skills — free.
Build Your Portfolio →