Application Security

API Security Best Practices

Protect Your APIs from OWASP Top 10 Vulnerabilities

18 min read

Table of Contents
  1. Why API Security Matters
  2. OWASP API Security Top 10
  3. Authentication & Authorization
  4. Input Validation
  5. Rate Limiting
  6. Encryption & HTTPS
  7. Logging & Monitoring
  8. Secure Code Examples

Why API Security Matters

APIs are the backbone of modern applications. They connect mobile apps, web frontends, microservices, and third-party integrations. But they are also the #1 attack vector for data breaches.

Alarming Statistics

OWASP API Security Top 10 (2023)

#VulnerabilityDescription
1Broken Object Level AuthAccessing other users' data by changing IDs
2Broken AuthenticationWeak tokens, no expiration, credential stuffing
3Broken Object Property AuthMass assignment, exposing sensitive fields
4Unrestricted Resource ConsumptionNo rate limiting, DoS attacks
5Broken Function Level AuthAdmin endpoints accessible to regular users
6Unrestricted Access to Sensitive FlowsAbusing business logic (coupons, voting)
7Server Side Request ForgeryAPI fetches attacker-controlled URLs
8Security MisconfigurationDebug mode, default credentials, CORS
9Improper Inventory ManagementDeprecated/shadow APIs still accessible
10Unsafe API ConsumptionTrusting data from third-party APIs

Authentication & Authorization

Use OAuth 2.0 + JWT

// Node.js - JWT Verification Middleware
const jwt = require('jsonwebtoken');

function authMiddleware(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
    
    if (!token) {
        return res.status(401).json({ error: 'No token provided' });
    }
    
    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        if (err) {
            return res.status(403).json({ error: 'Invalid token' });
        }
        
        // Check token expiration
        if (user.exp < Date.now() / 1000) {
            return res.status(403).json({ error: 'Token expired' });
        }
        
        req.user = user;
        next();
    });
}

// Apply to protected routes
app.get('/api/user/profile', authMiddleware, (req, res) => {
    res.json(req.user);
});
Best Practices

Object-Level Authorization (BOLA Prevention)

// Python Flask - Prevent BOLA
@app.route('/api/orders/<order_id>')
@login_required
def get_order(order_id):
    order = Order.query.get(order_id)
    
    if not order:
        return jsonify({"error": "Not found"}), 404
    
    # CRITICAL: Verify ownership before returning data
    if order.user_id != current_user.id:
        # Log potential attack
        log_security_event("BOLA attempt", current_user.id, order_id)
        return jsonify({"error": "Forbidden"}), 403
    
    return jsonify(order.to_dict())

Input Validation

Never trust user input. Validate everything on the server side.

// Express.js with express-validator
const { body, validationResult } = require('express-validator');

app.post('/api/users',
    // Validation rules
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }).matches(/[A-Z]/).matches(/[0-9]/),
    body('username').isAlphanumeric().trim().escape(),
    body('age').isInt({ min: 13, max: 120 }),
    
    (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
        }
        
        // Safe to process
        createUser(req.body);
    }
);
SQL Injection Prevention
// BAD - SQL Injection vulnerable
db.query(`SELECT * FROM users WHERE id = ${userId}`);

// GOOD - Parameterized query
db.query('SELECT * FROM users WHERE id = ?', [userId]);

Rate Limiting

Protect against abuse, brute force, and DoS attacks.

// Express.js Rate Limiting
const rateLimit = require('express-rate-limit');

// General API limiter
const apiLimiter = 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
});

// Stricter limiter for auth endpoints
const authLimiter = rateLimit({
    windowMs: 60 * 60 * 1000, // 1 hour
    max: 5, // 5 login attempts per hour
    message: { error: 'Account locked. Try again in 1 hour.' }
});

app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);

Encryption & HTTPS

# Nginx - Security Headers
server {
    listen 443 ssl http2;
    
    # TLS Configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
    
    # Security Headers
    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-XSS-Protection "1; mode=block" always;
}

Logging & Monitoring

You can't protect what you can't see. Log security-relevant events.

// Structured Security Logging
function logSecurityEvent(event, userId, details) {
    const log = {
        timestamp: new Date().toISOString(),
        event: event,
        userId: userId,
        ip: req.ip,
        userAgent: req.headers['user-agent'],
        details: details,
        severity: getSeverity(event)
    };
    
    // Send to SIEM (Splunk, ELK, etc.)
    securityLogger.info(JSON.stringify(log));
    
    // Alert on critical events
    if (log.severity === 'CRITICAL') {
        alertSecurityTeam(log);
    }
}

// Events to log
logSecurityEvent('LOGIN_SUCCESS', userId);
logSecurityEvent('LOGIN_FAILED', attemptedUser);
logSecurityEvent('PERMISSION_DENIED', userId, resource);
logSecurityEvent('RATE_LIMIT_EXCEEDED', userId);
logSecurityEvent('SUSPICIOUS_PAYLOAD', userId, payload);

Complete Secure API Example

// Complete Express.js Secure API Setup
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');

const app = express();

// Security Middleware
app.use(helmet()); // Sets security headers
app.use(cors({
    origin: ['https://myapp.com'],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    credentials: true
}));
app.use(express.json({ limit: '10kb' })); // Limit body size

// Rate Limiting
app.use(rateLimit({ windowMs: 15*60*1000, max: 100 }));

// Request Logging
app.use((req, res, next) => {
    console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
    next();
});

// Health Check (no auth required)
app.get('/health', (req, res) => res.json({ status: 'ok' }));

// Protected Routes
app.use('/api', authMiddleware);
app.get('/api/data', (req, res) => { /* */ });

// Error Handler - Don't leak stack traces
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({ error: 'Internal server error' });
});

app.listen(3000);
Further Reading

Security Checklist

Updated: December 2024