Back to Technology

API Development Series Part 6: API Security Hardening

January 31, 2026 Wasil Zafar 40 min read

Master API security hardening including OWASP API Security Top 10, Zero Trust architecture, WAF integration, input validation, rate limiting, and secrets management.

Table of Contents

  1. OWASP API Security Top 10
  2. Zero Trust Architecture
  3. Input Validation
  4. WAF Integration
  5. Gateway Rate Limiting
  6. Secrets Management
Series Navigation: This is Part 6 of the 17-part API Development Series. Review Part 5: Authentication & Authorization first.

OWASP API Security Top 10

2023 OWASP API Security Risks

OWASP maintains a list of the most critical API security risks. Understanding these is essential for building secure APIs.

OWASP API Top 10 (2023)

Critical Reference
# Risk Description
1 Broken Object Level Auth Accessing other users' data via ID manipulation
2 Broken Authentication Weak auth mechanisms, credential stuffing
3 Broken Object Property Auth Mass assignment, excessive data exposure
4 Unrestricted Resource Consumption Missing rate limits, DoS attacks
5 Broken Function Level Auth Accessing admin functions without permission
6 Unrestricted Access to Sensitive Flows Abuse of business flows (checkout, signup)
7 Server-Side Request Forgery Exploiting server to make internal requests
8 Security Misconfiguration Default configs, verbose errors, CORS issues
9 Improper Inventory Management Undocumented endpoints, old API versions
10 Unsafe API Consumption Trusting third-party API responses blindly

Broken Object Level Authorization (BOLA)

// VULNERABLE: No ownership check
app.get('/tasks/:id', async (req, res) => {
  const task = await Task.findById(req.params.id);
  return res.json(task); // Anyone can access any task!
});

// SECURE: Verify ownership
app.get('/tasks/:id', verifyJwt, async (req, res) => {
  const task = await Task.findById(req.params.id);
  
  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }
  
  // Check ownership or team membership
  if (task.ownerId !== req.user.sub && 
      !task.teamMembers.includes(req.user.sub)) {
    return res.status(403).json({
      type: 'https://api.example.com/errors/forbidden',
      title: 'Access Denied',
      status: 403,
      detail: 'You do not have access to this resource'
    });
  }
  
  return res.json(task);
});

Zero Trust Architecture

Zero Trust Principles

Never Trust, Always Verify:
  • Verify Explicitly: Authenticate every request, every time
  • Least Privilege: Grant minimum permissions needed
  • Assume Breach: Design as if attackers are already inside
// Zero Trust middleware stack
app.use('/api/*', [
  // 1. Validate request origin
  validateOrigin,
  
  // 2. Authenticate identity
  verifyJwt,
  
  // 3. Check authorization
  checkPermissions,
  
  // 4. Validate input
  validateRequest,
  
  // 5. Log everything
  auditLog,
  
  // 6. Rate limit
  rateLimit
]);

// Validate request origin
const validateOrigin = (req, res, next) => {
  const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
  const origin = req.headers.origin;
  
  if (origin && !allowedOrigins.includes(origin)) {
    return res.status(403).json({
      type: 'https://api.example.com/errors/origin-not-allowed',
      title: 'Origin Not Allowed',
      status: 403
    });
  }
  next();
};

Input Validation

Defense in Depth

Validate inputs at multiple layers: API gateway, application, and database.

const Joi = require('joi');

// Define strict schemas
const createTaskSchema = Joi.object({
  title: Joi.string()
    .min(1)
    .max(200)
    .pattern(/^[a-zA-Z0-9\s\-_.,:!?]+$/)  // Whitelist characters
    .required(),
  
  description: Joi.string()
    .max(2000)
    .allow('')
    .optional(),
  
  status: Joi.string()
    .valid('pending', 'in_progress', 'completed')
    .default('pending'),
  
  priority: Joi.string()
    .valid('low', 'medium', 'high', 'urgent')
    .default('medium'),
  
  due_date: Joi.date()
    .iso()
    .greater('now')
    .optional(),
  
  tags: Joi.array()
    .items(Joi.string().max(50))
    .max(10)
    .optional(),
  
  // Prevent mass assignment - only allow known fields
}).options({ stripUnknown: true });

// Validation middleware
const validate = (schema) => {
  return (req, res, next) => {
    const { error, value } = schema.validate(req.body, {
      abortEarly: false,  // Return all errors
      convert: true       // Type coercion
    });
    
    if (error) {
      const errors = error.details.map(d => ({
        field: d.path.join('.'),
        message: d.message,
        code: d.type
      }));
      
      return res.status(422).json({
        type: 'https://api.example.com/errors/validation',
        title: 'Validation Error',
        status: 422,
        errors
      });
    }
    
    req.body = value; // Use sanitized value
    next();
  };
};

SQL Injection Prevention

// VULNERABLE: String concatenation
const query = `SELECT * FROM tasks WHERE id = '${req.params.id}'`;

// SECURE: Parameterized queries
const task = await db.query(
  'SELECT * FROM tasks WHERE id = $1 AND owner_id = $2',
  [req.params.id, req.user.sub]
);

// With Knex.js query builder
const task = await knex('tasks')
  .where({ id: req.params.id, owner_id: req.user.sub })
  .first();

WAF Integration

Web Application Firewall Rules

WAFs provide an additional security layer at the edge, blocking attacks before they reach your application.

# AWS WAF Rule Example (Terraform)
resource "aws_wafv2_web_acl" "api_waf" {
  name  = "api-protection"
  scope = "REGIONAL"

  default_action {
    allow {}
  }

  # Block SQL injection
  rule {
    name     = "SQLiProtection"
    priority = 1
    
    statement {
      sqli_match_statement {
        field_to_match { body {} }
        text_transformation {
          priority = 1
          type     = "URL_DECODE"
        }
      }
    }
    
    action { block {} }
  }

  # Block XSS
  rule {
    name     = "XSSProtection"
    priority = 2
    
    statement {
      xss_match_statement {
        field_to_match { body {} }
        text_transformation {
          priority = 1
          type     = "HTML_ENTITY_DECODE"
        }
      }
    }
    
    action { block {} }
  }

  # Rate limiting
  rule {
    name     = "RateLimitRule"
    priority = 3
    
    statement {
      rate_based_statement {
        limit              = 2000
        aggregate_key_type = "IP"
      }
    }
    
    action { block {} }
  }
}

Gateway Rate Limiting

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');

const redis = new Redis(process.env.REDIS_URL);

// Tiered rate limiting
const rateLimitByTier = (tier) => {
  const limits = {
    free: { windowMs: 60000, max: 60 },     // 60/min
    pro: { windowMs: 60000, max: 600 },     // 600/min  
    enterprise: { windowMs: 60000, max: 6000 } // 6000/min
  };
  
  return rateLimit({
    store: new RedisStore({ client: redis }),
    windowMs: limits[tier].windowMs,
    max: limits[tier].max,
    keyGenerator: (req) => req.apiKey?.clientId || req.ip,
    handler: (req, res) => {
      res.status(429).json({
        type: 'https://api.example.com/errors/rate-limit',
        title: 'Rate Limit Exceeded',
        status: 429,
        detail: `You have exceeded ${limits[tier].max} requests per minute`
      });
    },
    headers: true // Send RateLimit-* headers
  });
};

// Apply based on API key tier
app.use('/api/*', (req, res, next) => {
  const tier = req.apiKey?.tier || 'free';
  rateLimitByTier(tier)(req, res, next);
});

Secrets Management

Never Hardcode Secrets

Critical Rules:
  • Never commit secrets to git (use .gitignore, git-secrets)
  • Use environment variables for local development
  • Use secrets managers for production (AWS Secrets Manager, HashiCorp Vault)
  • Rotate secrets regularly and on suspected compromise
// Using AWS Secrets Manager
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');

const client = new SecretsManagerClient({ region: 'us-east-1' });

async function getSecret(secretName) {
  const command = new GetSecretValueCommand({ SecretId: secretName });
  const response = await client.send(command);
  return JSON.parse(response.SecretString);
}

// Load secrets at startup
const secrets = await getSecret('prod/api/database');
const dbConfig = {
  host: secrets.host,
  user: secrets.username,
  password: secrets.password,
  database: secrets.database
};

Security Headers

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https:'],
    }
  },
  hsts: { maxAge: 31536000, includeSubDomains: true },
  noSniff: true,
  xssFilter: true,
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));

// CORS configuration
const cors = require('cors');
app.use(cors({
  origin: ['https://app.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
  credentials: true,
  maxAge: 86400
}));

Practice Exercises

Exercise 1: Input Validation

Beginner 45 minutes
  • Add Joi validation to all endpoints
  • Implement schema-based request validation
  • Return structured validation errors

Exercise 2: Security Headers

Intermediate 1 hour
  • Configure Helmet with strict CSP
  • Set up CORS properly
  • Verify headers with security scanner

Exercise 3: WAF Configuration

Advanced 2 hours
  • Set up AWS WAF with managed rules
  • Create custom SQLi/XSS rules
  • Implement IP-based rate limiting
  • Test with OWASP ZAP
Next Steps: In Part 7: AWS API Gateway, we'll build serverless APIs with Lambda integration, custom authorizers, and usage plans.
Technology