Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Security Best Practices

API Security Best Practices: 11 Essential Controls (2026)

11 API security best practices with working code examples: stop BOLA, harden JWTs, rate-limit auth, fix CORS misconfigs & block mass assignment. Full OWASP API Top 10 coverage.

Offensive360 Security Research Team — min read
API security REST API GraphQL OWASP API Security authentication rate limiting JWT api security best practices api security standards api protection REST API security API security controls API security guidelines

APIs have overtaken traditional web applications as the most common attack surface. The OWASP API Security Top 10 documents the most critical risks, and real-world breaches at companies like Twitter, Facebook, Peloton, and countless others originated from poorly secured API endpoints.

This guide covers 11 essential API security controls, what can go wrong without them, and how to implement each correctly.


1. Authenticate Every Request

Every API endpoint that handles user data or performs actions must require authentication. The two main approaches:

JWT (JSON Web Tokens)

// Middleware to validate JWT on every protected route
const verifyToken = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1]; // Bearer <token>

  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  try {
    // CRITICAL: always specify algorithm — never let JWT library auto-detect
    const decoded = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'], // Explicitly specify — prevents algorithm confusion attacks
    });
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
};

Common JWT mistakes to avoid:

  • algorithms: ['none'] — allows unsigned tokens
  • Using symmetric secret for tokens you didn’t issue yourself (use RS256 instead)
  • Not validating exp, nbf, iss, aud claims

API Keys

# API key validation middleware
def require_api_key(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        api_key = request.headers.get('X-API-Key')
        if not api_key:
            return jsonify({'error': 'API key required'}), 401

        # Constant-time comparison to prevent timing attacks
        if not hmac.compare_digest(api_key, os.environ['VALID_API_KEY']):
            return jsonify({'error': 'Invalid API key'}), 401

        return f(*args, **kwargs)
    return decorated_function

2. Implement Proper Authorization (Not Just Authentication)

Authentication confirms who is making the request. Authorization determines what they’re allowed to do.

The most common API vulnerability: Broken Object Level Authorization (BOLA/IDOR) — accessing another user’s data by changing an ID.

# VULNERABLE — only checks authentication, not ownership
@app.route('/api/orders/<order_id>')
@require_auth
def get_order(order_id):
    order = Order.query.get(order_id)  # Any authenticated user can access any order
    return jsonify(order.to_dict())

# SECURE — checks that the order belongs to the requesting user
@app.route('/api/orders/<order_id>')
@require_auth
def get_order(order_id):
    order = Order.query.filter_by(
        id=order_id,
        user_id=current_user.id  # ← Ownership check
    ).first_or_404()
    return jsonify(order.to_dict())

3. Rate Limiting on All Endpoints

Without rate limiting, APIs are vulnerable to:

  • Brute force attacks on authentication endpoints
  • Credential stuffing
  • Scraping of user data
  • DoS via expensive operations
// Express — per-endpoint rate limiting
const rateLimit = require('express-rate-limit');

// Strict limits for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10, // 10 attempts per IP per 15 minutes
  message: { error: 'Too many login attempts. Try again in 15 minutes.' },
  standardHeaders: true,
});

// More lenient for general API
const apiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100,
});

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

4. Validate and Sanitize All Input

Never trust API input — validate type, format, length, and range for every parameter.

// Zod schema validation (TypeScript)
import { z } from 'zod';

const CreateUserSchema = z.object({
  email: z.string().email().max(255),
  name: z.string().min(1).max(100).regex(/^[a-zA-Z\s'-]+$/),
  age: z.number().int().min(0).max(150),
  role: z.enum(['user', 'viewer']), // Whitelist allowed values
});

app.post('/api/users', async (req, res) => {
  const result = CreateUserSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.issues });
  }
  // result.data is now type-safe and validated
  await createUser(result.data);
});

5. Avoid Excessive Data Exposure

APIs frequently return more data than the client needs, exposing sensitive fields:

# VULNERABLE — returns entire user object including password hash, internal flags
@app.route('/api/users/<user_id>')
def get_user(user_id):
    user = User.query.get_or_404(user_id)
    return jsonify(user.__dict__)  # Leaks password_hash, is_admin, internal_notes

# SECURE — explicit field selection
@app.route('/api/users/<user_id>')
def get_user(user_id):
    user = User.query.get_or_404(user_id)
    return jsonify({
        'id': user.id,
        'name': user.display_name,
        'avatar_url': user.avatar_url,
        # password_hash, is_admin, etc. intentionally excluded
    })

This also applies to error messages — never expose stack traces, database schema details, or internal paths in API error responses.


6. Use HTTPS and Enforce It

All API traffic must be encrypted. Never allow HTTP fallback for API endpoints.

# Nginx — redirect HTTP to HTTPS and set HSTS
server {
    listen 80;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # 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;
}

7. Configure CORS Correctly

Misconfigured CORS is a common API vulnerability that allows malicious websites to make authenticated requests on behalf of your users.

// DANGEROUS — allows any origin to make credentialed requests
app.use(cors({
  origin: '*',
  credentials: true, // This combination is actually blocked by browsers, but '*' alone is still dangerous
}));

// SECURE — explicit allowlist
const allowedOrigins = [
  'https://app.yourcompany.com',
  'https://yourcompany.com',
];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));

8. Protect Against Mass Assignment

Mass assignment occurs when API endpoints automatically bind all request body properties to a model, including fields that should never be user-controlled.

# VULNERABLE — user can set is_admin=True in the request body
@app.route('/api/profile', methods=['PUT'])
def update_profile():
    user = current_user
    user.update(**request.json)  # Accepts ANY field, including is_admin, credit_balance
    db.session.commit()

# SECURE — explicit allowlist of mutable fields
ALLOWED_PROFILE_FIELDS = {'name', 'bio', 'avatar_url', 'notification_preferences'}

@app.route('/api/profile', methods=['PUT'])
def update_profile():
    data = {k: v for k, v in request.json.items() if k in ALLOWED_PROFILE_FIELDS}
    current_user.update(**data)
    db.session.commit()

9. Implement Security Headers

# Flask — security headers on all API responses
@app.after_request
def add_security_headers(response):
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['Cache-Control'] = 'no-store'
    response.headers['Pragma'] = 'no-cache'
    # Remove server version disclosure
    response.headers.pop('Server', None)
    response.headers.pop('X-Powered-By', None)
    return response

10. Secure GraphQL APIs

GraphQL requires additional security controls beyond standard REST API hardening:

// Disable introspection in production — prevents schema enumeration
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',

  // Depth limiting — prevents deeply nested queries that cause DoS
  validationRules: [
    depthLimit(5),
    costAnalysis({ maximumCost: 1000 }),
  ],
});

// Field-level authorization
const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      if (!context.user) throw new AuthenticationError('Login required');
      if (context.user.id !== id && !context.user.isAdmin) {
        throw new ForbiddenError('Access denied');
      }
      return User.findById(id);
    },
  },
};

11. Log and Monitor API Activity

Centralized logging of API activity enables detection of attacks in progress:

import logging
from functools import wraps

def log_api_call(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        logger.info({
            'event': 'api_call',
            'endpoint': request.path,
            'method': request.method,
            'user_id': getattr(current_user, 'id', None),
            'ip': request.remote_addr,
            'user_agent': request.user_agent.string,
        })

        try:
            response = f(*args, **kwargs)
            logger.info({'event': 'api_response', 'status': response.status_code})
            return response
        except Exception as e:
            logger.error({'event': 'api_error', 'error': str(e)})
            raise
    return wrapper

Monitor for:

  • More than 5 authentication failures from one IP in 5 minutes
  • Successful login followed by rapid enumeration of object IDs
  • Unusual data volume in responses (potential mass data extraction)
  • Requests to admin endpoints from non-admin accounts

OWASP API Security Top 10 Reference

#RiskControl
API1Broken Object Level AuthorizationPractice 2 (ownership checks)
API2Broken AuthenticationPractice 1 (JWT/API key validation)
API3Broken Object Property Level AuthorizationPractice 8 (mass assignment protection)
API4Unrestricted Resource ConsumptionPractice 3 (rate limiting)
API5Broken Function Level AuthorizationPractice 2 (role-based access)
API6Unrestricted Access to Sensitive Business FlowsPractice 3 + business logic testing
API7Server-Side Request ForgeryInput validation + network egress control
API8Security MisconfigurationPractices 6, 7, 9
API9Improper Inventory ManagementAsset inventory + API versioning
API10Unsafe Consumption of APIsValidate third-party API responses

Offensive360 DAST tests your live API endpoints for all OWASP API Security Top 10 risks. Run a DAST scan on your API or contact us to discuss API security testing.

Offensive360 Security Research Team

Application Security Research

Updated March 27, 2026

Find vulnerabilities before attackers do

Run Offensive360 SAST and DAST against your applications and get a full vulnerability report in minutes.