API Security Best Practices: How to Secure Your APIs Against Real-World Attacks

API Security Best Practices: How to Secure Your APIs Against Real-World Attacks

APIs are the number one attack surface for modern web applications. OWASP’s API Security Top 10 describes vulnerabilities that attackers exploit daily — broken authentication, excessive data exposure, mass assignment, and injection attacks. This guide covers each threat with the actual code-level defenses that prevent them, not just theory.

TL;DR: Validate every input server-side. Use short-lived JWTs with refresh tokens. Implement rate limiting on every endpoint. Return minimum data (never expose internal fields). Use parameterized queries — never string concatenation. Set CORS to specific origins. Log auth failures. Never trust client-provided IDs for authorization.

OWASP API Top 10 — the real threats

// 1. Broken Object Level Authorization (BOLA/IDOR) — most common API flaw
// Attacker changes ID in URL to access other users' data

// VULNERABLE:
app.get('/invoices/:id', auth, async (req, res) => {
  const invoice = await db.getInvoice(req.params.id);
  res.json(invoice); // Returns invoice regardless of who owns it!
});
// Attacker: GET /invoices/12345 (not their invoice) → gets it!

// SECURE:
app.get('/invoices/:id', auth, async (req, res) => {
  const invoice = await db.query(
    'SELECT * FROM invoices WHERE id = $1 AND user_id = $2',
    [req.params.id, req.user.id] // ALWAYS scope to authenticated user
  );
  if (!invoice) return res.status(404).json({ error: 'Not found' });
  res.json(invoice);
});

// 2. Broken Authentication
// VULNERABLE: no token expiry, weak secret, algorithm=none
const token = jwt.sign(payload, 'weak');
// SECURE:
const token = jwt.sign(payload, process.env.JWT_SECRET, {
  expiresIn: '15m',    // Short-lived
  algorithm: 'HS256'   // Always specify algorithm explicitly
});
// ALWAYS verify: jwt.verify(token, secret, { algorithms: ['HS256'] })

Input validation and injection prevention

import { z } from 'zod';

// Validate EVERYTHING from the client — never trust request data
const CreateUserSchema = z.object({
  name: z.string().min(1).max(100).trim(),
  email: z.string().email().toLowerCase(),
  age: z.number().int().min(18).max(120),
  role: z.enum(['user', 'moderator']).default('user'),
  // NEVER accept: 'admin', isAdmin: true, userId, internalId, etc.
});

app.post('/users', async (req, res) => {
  const parsed = CreateUserSchema.safeParse(req.body);
  if (!parsed.success) return res.status(422).json({ error: parsed.error.format() });

  const { name, email, age, role } = parsed.data; // Only allowed fields
  await db.createUser({ name, email, age, role }); // Mass assignment prevented
});

// SQL injection prevention — ALWAYS parameterized queries
// VULNERABLE:
const user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
// Attacker: email = "x' OR 1=1 --" → returns all users!

// SECURE:
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
// $1 is a parameter — never concatenated into SQL

Rate limiting and DDoS protection

import rateLimit from 'express-rate-limit';

// Different limits for different endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10,                  // 10 attempts per IP per 15 min
  standardHeaders: true,    // Return rate limit headers
  message: { error: 'Too many attempts, try again in 15 minutes' },
});

const apiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100,            // 100 requests per minute
});

// Apply different limits to different routes
app.use('/auth/', authLimiter);  // Stricter: prevent brute force
app.use('/api/', apiLimiter);   // Standard: prevent abuse

// Also limit by API key (not just IP):
const keyLimiter = rateLimit({
  keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
  max: 1000, // Per API key per minute
});

CORS and security headers

import cors from 'cors';
import helmet from 'helmet';

// WRONG: allow all origins
app.use(cors()); // Access-Control-Allow-Origin: *
// Anyone can call your API with a browser request from any domain!

// CORRECT: specific allowed origins
const allowedOrigins = [
  'https://myapp.com',
  'https://admin.myapp.com',
];

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

// Helmet: sets secure HTTP headers automatically
app.use(helmet()); // Sets: X-Content-Type-Options, X-Frame-Options,
                   // Content-Security-Policy, Strict-Transport-Security, etc.

API security checklist

  • ✅ BOLA: always scope DB queries to authenticated user’s ID
  • ✅ Validate all inputs with schema validation (Zod, Joi, Yup)
  • ✅ Parameterized queries — never string-concatenated SQL
  • ✅ Short-lived JWTs (15m access + 7d refresh) with explicit algorithm
  • ✅ Rate limiting on auth endpoints (10 req/15min) and all API endpoints
  • ✅ CORS: specific origins, not wildcard
  • ✅ Helmet.js for security headers
  • ✅ Log auth failures, flag suspicious patterns
  • ❌ Never trust client-provided user IDs, roles, or permissions
  • ❌ Never return stack traces, SQL queries, or internal paths in errors

API security connects to the REST API design guide — security decisions should be made at design time, not retrofitted. The AWS S3 presigned URL guide shows how authentication works at the infrastructure layer. External reference: OWASP API Security Top 10.

Recommended Reading

Designing Data-Intensive Applications — The essential book every senior developer needs. Covers distributed systems, databases, and production architecture.

The Pragmatic Programmer — Timeless engineering wisdom for writing better, more maintainable code at any level.

Affiliate links. We earn a small commission at no extra cost to you.

Free Weekly Newsletter

🚀 Don’t Miss the Next Cheat Code

Join 1,000+ senior developers getting expert-level JS, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.

✓ No spam✓ Unsubscribe anytime✓ Expert-level only

Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply