API Security Best Practices

Secure your REST APIs against common attacks

API and network security
By FixTheVuln Team Peer-reviewed security content Sources: CISA, NVD, OWASP

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
Why API Security Matters: APIs are the backbone of modern applications. They're also prime targets for attackers - exposing sensitive data, business logic, and backend systems. A single vulnerable API endpoint can compromise your entire infrastructure.

OWASP API Security Top 10 (2023)

# Vulnerability Description
API1Broken Object Level AuthorizationAPIs exposing endpoints that handle object IDs without proper authorization
API2Broken AuthenticationWeak authentication mechanisms allowing attackers to compromise tokens
API3Broken Object Property Level AuthorizationExposing or allowing modification of sensitive object properties
API4Unrestricted Resource ConsumptionNo limits on API requests allowing DoS attacks
API5Broken Function Level AuthorizationComplex access control policies leading to authorization flaws
API6Unrestricted Access to Sensitive Business FlowsExposing business flows without considering abuse potential
API7Server Side Request Forgery (SSRF)API fetches remote resources without validating user-supplied URLs
API8Security MisconfigurationInsecure default configurations, verbose errors, unnecessary features
API9Improper Inventory ManagementExposed old API versions, undocumented endpoints
API10Unsafe Consumption of APIsTrusting 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

Remember: Security is not a one-time task. Regularly audit your APIs, keep dependencies updated, and conduct penetration testing to identify vulnerabilities before attackers do.

Related Resources

🎟️ JWT Decoder Decode JSON Web Tokens 🔒 OWASP Top 10 Critical web security risks 🔐 SSL/TLS Hardening Secure HTTPS configuration

Test Your Knowledge

Security+ Practice Quiz CySA+ Practice Quiz
← Back to Tools

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 Planner

60+ 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 →