API Security Best Practices Checklist

Meta Description: Secure your REST API with this comprehensive checklist covering authentication, authorization, encryption, rate limiting, input validation, and more. Keywords: api security, security best practices, api security checklist, secure api design, api hardening Word Count: ~2,300 words Your API is live. But is it secure? Security isn'

TRY NANO BANANA FOR FREE

API Security Best Practices Checklist

TRY NANO BANANA FOR FREE
Contents

Meta Description: Secure your REST API with this comprehensive checklist covering authentication, authorization, encryption, rate limiting, input validation, and more.

Keywords: api security, security best practices, api security checklist, secure api design, api hardening

Word Count: ~2,300 words


Your API is live. But is it secure?

Security isn't one feature—it's a collection of practices. Miss one, and attackers find a way in.

Here's a complete security checklist for REST APIs.

Authentication & Authorization

✓ Use HTTPS Everywhere

Never use HTTP for APIs. Always use HTTPS (TLS 1.2 or higher).

// Redirect HTTP to HTTPS
app.use((req, res, next) => {
  if (req.protocol !== 'https' && process.env.NODE_ENV === 'production') {
    return res.redirect(301, `https://${req.hostname}${req.url}`);
  }
  next();
});

✓ Implement Proper Authentication

Choose the right method: - API Keys: Server-to-server - OAuth 2.0: User authorization - JWT: Stateless authentication

Never accept credentials in URLs:

❌ GET /api/pets?api_key=abc123
✓ GET /api/pets
   Authorization: Bearer abc123

✓ Verify Object-Level Authorization

Always check if the user owns or has permission to access the resource:

app.get('/v1/pets/:id', async (req, res) => {
  const pet = await db.pets.findById(req.params.id);

  if (!pet) {
    return res.status(404).json({ error: 'Not found' });
  }

  // Check ownership
  if (pet.ownerId !== req.auth.userId && !req.auth.isAdmin) {
    return res.status(403).json({ error: 'Access denied' });
  }

  res.json(pet);
});

✓ Implement Function-Level Authorization

Protect admin and privileged endpoints:

function requireAdmin(req, res, next) {
  if (!req.auth.scopes.includes('admin:all')) {
    return res.status(403).json({ error: 'Admin access required' });
  }
  next();
}

app.delete('/v1/admin/users/:id', requireAdmin, async (req, res) => {
  // Only admins can delete users
});

✓ Use Short-Lived Tokens

Tokens should expire:

const token = jwt.sign(
  { userId: user.id },
  JWT_SECRET,
  { expiresIn: '1h' } // Expire after 1 hour
);

Provide refresh tokens for long-lived sessions:

const accessToken = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: user.id }, REFRESH_SECRET, { expiresIn: '7d' });

Input Validation

✓ Validate All Input

Never trust client input. Validate everything:

const { body, validationResult } = require('express-validator');

app.post('/v1/pets',
  body('name').isString().trim().isLength({ min: 1, max: 100 }),
  body('species').isIn(['DOG', 'CAT', 'BIRD', 'RABBIT']),
  body('age').optional().isInt({ min: 0, max: 30 }),
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }

    // Process valid input
  }
);

✓ Sanitize Input

Remove dangerous characters:

const sanitizeHtml = require('sanitize-html');

function sanitizeInput(input) {
  if (typeof input === 'string') {
    return sanitizeHtml(input, {
      allowedTags: [], // Strip all HTML
      allowedAttributes: {}
    });
  }
  return input;
}

✓ Prevent SQL Injection

Use parameterized queries:

// ❌ Vulnerable to SQL injection
const pets = await db.query(`SELECT * FROM pets WHERE name = '${req.query.name}'`);

// ✓ Safe with parameterized query
const pets = await db.query('SELECT * FROM pets WHERE name = ?', [req.query.name]);

✓ Prevent NoSQL Injection

Validate types in NoSQL queries:

// ❌ Vulnerable
const user = await db.users.findOne({ email: req.body.email });

// ✓ Safe
const user = await db.users.findOne({
  email: String(req.body.email) // Ensure it's a string
});

✓ Limit Request Size

Prevent large payload attacks:

app.use(express.json({ limit: '1mb' })); // Max 1MB request body
app.use(express.urlencoded({ limit: '1mb', extended: true }));

Rate Limiting

✓ Implement Rate Limiting

Protect against brute force and DoS:

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  message: { error: 'Too many requests' }
});

app.use('/v1/', limiter);

✓ Stricter Limits for Sensitive Endpoints

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // Only 5 login attempts per 15 minutes
  skipSuccessfulRequests: true
});

app.post('/v1/auth/login', authLimiter, async (req, res) => {
  // Handle login
});

✓ Return Rate Limit Headers

app.use((req, res, next) => {
  res.setHeader('X-RateLimit-Limit', '100');
  res.setHeader('X-RateLimit-Remaining', '95');
  res.setHeader('X-RateLimit-Reset', '1678886400');
  next();
});

Data Protection

✓ Never Log Sensitive Data

// ❌ Don't log passwords, tokens, or PII
console.log('Login attempt:', req.body); // Contains password!

// ✓ Log safely
console.log('Login attempt:', { email: req.body.email });

✓ Hash Passwords

Never store plain text passwords:

const bcrypt = require('bcrypt');

async function hashPassword(password) {
  return await bcrypt.hash(password, 12); // 12 rounds
}

async function verifyPassword(password, hash) {
  return await bcrypt.compare(password, hash);
}

✓ Encrypt Sensitive Data at Rest

const crypto = require('crypto');

function encrypt(text, key) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
  const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  return { encrypted, iv, tag };
}

function decrypt(encrypted, iv, tag, key) {
  const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
  decipher.setAuthTag(tag);
  return decipher.update(encrypted) + decipher.final('utf8');
}

✓ Filter Sensitive Fields from Responses

function sanitizeUser(user) {
  const { passwordHash, ssn, creditCard, ...safeUser } = user;
  return safeUser;
}

app.get('/v1/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);
  res.json(sanitizeUser(user));
});

Error Handling

✓ Don't Expose Stack Traces

// ❌ Exposes internal details
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.stack });
});

// ✓ Generic error message
app.use((err, req, res, next) => {
  console.error(err); // Log internally
  res.status(500).json({
    type: 'https://api.petstoreapi.com/errors/internal-error',
    title: 'Internal Server Error',
    status: 500,
    detail: 'An unexpected error occurred'
  });
});

✓ Use Standard Error Format

Follow RFC 9457:

function errorResponse(type, title, status, detail) {
  return {
    type: `https://api.petstoreapi.com/errors/${type}`,
    title,
    status,
    detail
  };
}

Headers & CORS

✓ Set Security Headers

const helmet = require('helmet');

app.use(helmet()); // Sets multiple security headers

// Or manually:
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  next();
});

✓ Configure CORS Properly

const cors = require('cors');

app.use(cors({
  origin: ['https://myapp.com'], // Specific origins, not '*'
  credentials: true,
  maxAge: 86400
}));

Monitoring & Logging

✓ Log Security Events

function logSecurityEvent(event, details) {
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    event,
    ...details
  }));
}

// Log failed auth attempts
app.post('/v1/auth/login', async (req, res) => {
  const user = await authenticateUser(req.body);

  if (!user) {
    logSecurityEvent('failed_login', {
      email: req.body.email,
      ip: req.ip
    });
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Success
});

✓ Monitor for Anomalies

Track: - Failed authentication attempts - Rate limit violations - Unusual access patterns - Privilege escalation attempts

✓ Implement Audit Logs

async function auditLog(action, userId, resource, details) {
  await db.auditLogs.create({
    timestamp: new Date(),
    action,
    userId,
    resource,
    details,
    ip: req.ip
  });
}

app.delete('/v1/pets/:id', async (req, res) => {
  await deletePet(req.params.id);

  await auditLog('delete_pet', req.auth.userId, `pet:${req.params.id}`, {
    petName: pet.name
  });

  res.status(204).send();
});

Dependencies & Updates

✓ Keep Dependencies Updated

npm audit
npm audit fix

✓ Use Dependency Scanning

npm install -g snyk
snyk test

✓ Pin Dependency Versions

{
  "dependencies": {
    "express": "4.18.2", // Exact version, not "^4.18.2"
  }
}

Testing

✓ Test Security Controls

describe('Authorization', () => {
  it('should deny access to other users pets', async () => {
    const response = await request(app)
      .get('/v1/pets/123')
      .set('Authorization', `Bearer ${otherUserToken}`);

    expect(response.status).toBe(403);
  });

  it('should require authentication', async () => {
    const response = await request(app)
      .get('/v1/pets/123');

    expect(response.status).toBe(401);
  });
});

✓ Perform Penetration Testing

Regularly test for: - SQL injection - XSS - CSRF - Authentication bypass - Authorization bypass

Deployment

✓ Use Environment Variables

// ❌ Don't hardcode secrets
const JWT_SECRET = 'my-secret-key';

// ✓ Use environment variables
const JWT_SECRET = process.env.JWT_SECRET;

if (!JWT_SECRET) {
  throw new Error('JWT_SECRET environment variable is required');
}

✓ Disable Debug Mode in Production

if (process.env.NODE_ENV === 'production') {
  app.set('env', 'production');
  app.disable('x-powered-by');
}

✓ Use a WAF (Web Application Firewall)

Deploy behind a WAF like: - AWS WAF - Cloudflare - Akamai

Security Checklist Summary

Authentication & Authorization- [ ] HTTPS everywhere - [ ] Proper authentication method - [ ] Object-level authorization - [ ] Function-level authorization - [ ] Short-lived tokens

Input Validation- [ ] Validate all input - [ ] Sanitize input - [ ] Prevent SQL injection - [ ] Prevent NoSQL injection - [ ] Limit request size

Rate Limiting- [ ] Global rate limits - [ ] Endpoint-specific limits - [ ] Rate limit headers

Data Protection- [ ] No sensitive data in logs - [ ] Hash passwords - [ ] Encrypt sensitive data - [ ] Filter response fields

Error Handling- [ ] No stack traces exposed - [ ] Standard error format

Headers & CORS- [ ] Security headers - [ ] Proper CORS configuration

Monitoring- [ ] Log security events - [ ] Monitor anomalies - [ ] Audit logs

Dependencies- [ ] Keep dependencies updated - [ ] Dependency scanning - [ ] Pin versions

Testing- [ ] Security tests - [ ] Penetration testing

Deployment- [ ] Environment variables - [ ] Disable debug mode - [ ] Use WAF

Security is ongoing. Review this checklist regularly and stay updated on new vulnerabilities.


Related Articles: - OWASP Top 10 API Security- API Keys vs OAuth vs JWT- CORS Explained