How to Secure REST APIs Against the OWASP Top 10

Meta Description: Protect your REST API from the OWASP Top 10 vulnerabilities. Learn practical defenses against injection, broken auth, excessive data exposure, and more. Keywords: api security, owasp top 10, rest api security, api vulnerabilities, broken authentication, injection attacks Word Count: ~2,500 words The OWASP API Security Top 10

TRY NANO BANANA FOR FREE

How to Secure REST APIs Against the OWASP Top 10

TRY NANO BANANA FOR FREE
Contents

Meta Description: Protect your REST API from the OWASP Top 10 vulnerabilities. Learn practical defenses against injection, broken auth, excessive data exposure, and more.

Keywords: api security, owasp top 10, rest api security, api vulnerabilities, broken authentication, injection attacks

Word Count: ~2,500 words


The OWASP API Security Top 10 lists the most critical API vulnerabilities. These aren't theoretical risks—they're the vulnerabilities attackers exploit in real systems.

Here's how to defend against each one.

1. Broken Object Level Authorization (BOLA)

The most common API vulnerability. Users access objects they don't own.

Vulnerable:

app.get('/v1/pets/:id', async (req, res) => {
  const pet = await db.pets.findById(req.params.id);
  res.json(pet); // Returns any pet, regardless of ownership
});

Attacker changes the ID to access other users' pets:

GET /v1/pets/123  ← Their pet
GET /v1/pets/456  ← Someone else's pet (unauthorized)

Fixed:

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: 'Pet not found' });
  }

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

  res.json(pet);
});

Rule: Always verify the authenticated user owns or has permission to access the requested object.

2. Broken Authentication

Weak authentication mechanisms allow attackers to impersonate users.

Common issues: - Weak passwords allowed - No rate limiting on login - Tokens never expire - Tokens not invalidated on logout

Fixes:

Rate limit authentication endpoints:

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

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10, // 10 attempts per window
  message: { error: 'Too many login attempts' }
});

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

Use short-lived tokens:

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

Invalidate tokens on logout:

// Token blocklist
const blocklist = new Set();

app.post('/v1/auth/logout', (req, res) => {
  blocklist.add(req.token.jti); // Add token ID to blocklist
  res.status(204).send();
});

// Check blocklist on every request
function validateToken(token) {
  const decoded = jwt.verify(token, JWT_SECRET);
  if (blocklist.has(decoded.jti)) {
    throw new Error('Token revoked');
  }
  return decoded;
}

3. Broken Object Property Level Authorization

Users can read or modify object properties they shouldn't access.

Vulnerable (mass assignment):

app.put('/v1/users/:id', async (req, res) => {
  // Attacker sends: { "role": "admin", "name": "John" }
  const user = await db.users.update(req.params.id, req.body);
  res.json(user);
});

Attacker escalates their own privileges by sending role: admin.

Fixed (allowlist fields):

app.put('/v1/users/:id', async (req, res) => {
  // Only allow specific fields
  const allowedFields = ['name', 'email', 'bio', 'avatar'];
  const updateData = {};

  allowedFields.forEach(field => {
    if (req.body[field] !== undefined) {
      updateData[field] = req.body[field];
    }
  });

  const user = await db.users.update(req.params.id, updateData);
  res.json(user);
});

Also fix response filtering:

function sanitizeUser(user) {
  // Remove sensitive fields from response
  const { passwordHash, internalNotes, adminFlags, ...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));
});

4. Unrestricted Resource Consumption

No limits on request size, rate, or resource usage.

Vulnerable:

app.post('/v1/pets/search', async (req, res) => {
  // No limit on results
  const pets = await db.pets.findMany(req.body.filters);
  res.json(pets); // Could return millions of records
});

Fixed:

app.post('/v1/pets/search', async (req, res) => {
  const limit = Math.min(req.body.limit || 20, 100); // Max 100 results
  const pets = await db.pets.findMany({
    ...req.body.filters,
    limit: limit
  });
  res.json({ data: pets, limit });
});

Add request size limits:

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

Add query complexity limits (for GraphQL):

const depthLimit = require('graphql-depth-limit');
const { createComplexityLimitRule } = require('graphql-validation-complexity');

app.use('/graphql', graphqlHTTP({
  schema,
  validationRules: [
    depthLimit(5), // Max 5 levels deep
    createComplexityLimitRule(1000) // Max complexity score
  ]
}));

5. Broken Function Level Authorization

Users access admin or privileged functions.

Vulnerable:

app.delete('/v1/admin/users/:id', async (req, res) => {
  // No admin check!
  await db.users.delete(req.params.id);
  res.status(204).send();
});

Fixed:

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) => {
  await db.users.delete(req.params.id);
  res.status(204).send();
});

Don't rely on obscurity:

// BAD: Hiding admin endpoints
app.delete('/v1/xk9m2p/users/:id', ...); // Security through obscurity

// GOOD: Proper authorization
app.delete('/v1/admin/users/:id', requireAdmin, ...);

6. Unrestricted Access to Sensitive Business Flows

Attackers abuse legitimate API flows at scale.

Example: Automated pet adoption applications

// Vulnerable: No rate limiting on applications
app.post('/v1/pets/:id/applications', async (req, res) => {
  const application = await createApplication(req.body);
  res.status(201).json(application);
});

Attacker submits thousands of applications to monopolize pets.

Fixed:

// Rate limit per user
const applicationLimiter = rateLimit({
  windowMs: 24 * 60 * 60 * 1000, // 24 hours
  max: 5, // 5 applications per day
  keyGenerator: (req) => req.auth.userId
});

// Limit pending applications
app.post('/v1/pets/:id/applications', applicationLimiter, async (req, res) => {
  const pendingCount = await db.applications.count({
    userId: req.auth.userId,
    status: 'PENDING'
  });

  if (pendingCount >= 3) {
    return res.status(429).json({
      error: 'Too many pending applications',
      detail: 'You can have at most 3 pending applications'
    });
  }

  const application = await createApplication(req.body);
  res.status(201).json(application);
});

7. Server-Side Request Forgery (SSRF)

API fetches URLs provided by users, allowing access to internal systems.

Vulnerable:

app.post('/v1/pets/import', async (req, res) => {
  // Attacker sends: { "imageUrl": "http://169.254.169.254/latest/meta-data/" }
  const image = await fetch(req.body.imageUrl);
  // Fetches AWS metadata, internal services, etc.
});

Fixed:

const { URL } = require('url');

function isSafeUrl(urlString) {
  try {
    const url = new URL(urlString);

    // Only allow HTTPS
    if (url.protocol !== 'https:') return false;

    // Block private IP ranges
    const hostname = url.hostname;
    const privateRanges = [
      /^localhost$/,
      /^127\./,
      /^10\./,
      /^172\.(1[6-9]|2[0-9]|3[01])\./,
      /^192\.168\./,
      /^169\.254\./ // AWS metadata
    ];

    if (privateRanges.some(range => range.test(hostname))) {
      return false;
    }

    return true;
  } catch {
    return false;
  }
}

app.post('/v1/pets/import', async (req, res) => {
  if (!isSafeUrl(req.body.imageUrl)) {
    return res.status(400).json({ error: 'Invalid image URL' });
  }

  const image = await fetch(req.body.imageUrl);
  // Safe to proceed
});

8. Security Misconfiguration

Default settings, verbose errors, and missing security headers.

Add security headers:

const helmet = require('helmet');
app.use(helmet());

// Or manually:
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  res.setHeader('Content-Security-Policy', "default-src 'none'");
  next();
});

Don't expose stack traces:

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

// GOOD
app.use((err, req, res, next) => {
  console.error(err); // Log internally

  res.status(500).json({
    type: 'https://petstoreapi.com/errors/internal-error',
    title: 'Internal Server Error',
    status: 500
  });
});

Disable unnecessary HTTP methods:

app.use((req, res, next) => {
  const allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
  if (!allowedMethods.includes(req.method)) {
    return res.status(405).json({ error: 'Method not allowed' });
  }
  next();
});

9. Improper Inventory Management

Undocumented or forgotten API versions and endpoints.

Maintain an API inventory:

# api-inventory.yaml
versions:
  v1:
    status: deprecated
    sunset: 2027-01-01
    endpoints: 45
  v2:
    status: current
    endpoints: 52

environments:
  production: https://api.petstoreapi.com
  staging: https://staging.api.petstoreapi.com
  development: http://localhost:3000

Add deprecation headers:

app.use('/v1', (req, res, next) => {
  res.setHeader('Deprecation', 'true');
  res.setHeader('Sunset', 'Sat, 01 Jan 2027 00:00:00 GMT');
  res.setHeader('Link', '</v2>; rel="successor-version"');
  next();
});

Disable debug endpoints in production:

if (process.env.NODE_ENV !== 'production') {
  app.get('/debug/config', (req, res) => {
    res.json(config);
  });
}

10. Unsafe Consumption of APIs

Trusting third-party API responses without validation.

Vulnerable:

async function getBreedInfo(breed) {
  const response = await fetch(`https://dog-api.example.com/breeds/${breed}`);
  const data = await response.json();

  // Trusting external data directly
  return {
    name: data.name,
    description: data.description,
    imageUrl: data.imageUrl // Could be malicious URL
  };
}

Fixed:

async function getBreedInfo(breed) {
  const response = await fetch(`https://dog-api.example.com/breeds/${breed}`);

  if (!response.ok) {
    throw new Error(`External API error: ${response.status}`);
  }

  const data = await response.json();

  // Validate and sanitize external data
  return {
    name: sanitizeString(data.name, { maxLength: 100 }),
    description: sanitizeString(data.description, { maxLength: 1000 }),
    imageUrl: isSafeUrl(data.imageUrl) ? data.imageUrl : null
  };
}

function sanitizeString(value, options = {}) {
  if (typeof value !== 'string') return '';
  let sanitized = value.trim();
  if (options.maxLength) {
    sanitized = sanitized.substring(0, options.maxLength);
  }
  return sanitized;
}

Security Checklist

Use this checklist for every API endpoint:

Authorization: - [ ] Verify user owns the requested resource (BOLA) - [ ] Check function-level permissions - [ ] Allowlist fields for updates (mass assignment) - [ ] Filter sensitive fields from responses

Authentication: - [ ] Rate limit auth endpoints - [ ] Use short-lived tokens - [ ] Invalidate tokens on logout

Input Validation: - [ ] Validate all input types and formats - [ ] Limit request body size - [ ] Sanitize strings for injection

Rate Limiting: - [ ] Limit requests per user - [ ] Limit results per request - [ ] Limit business flow operations

Configuration: - [ ] Add security headers - [ ] Hide stack traces in production - [ ] Disable debug endpoints in production

Testing Your API Security

Use these tools to test your defenses:

OWASP ZAP: Automated security scanning Burp Suite: Manual security testing Postman: API testing with security checks Apidog: API testing with security validation

Run security tests in your CI/CD pipeline:

# GitHub Actions
- name: Security Scan
  run: |
    docker run -t owasp/zap2docker-stable zap-api-scan.py \
      -t https://staging.api.petstoreapi.com/openapi.json \
      -f openapi

Catch vulnerabilities before they reach production.


Meta Tags: - Title: How to Secure REST APIs Against the OWASP Top 10 - Description: Protect your REST API from the OWASP Top 10 vulnerabilities. Learn practical defenses against injection, broken auth, excessive data exposure, and more. - Keywords: api security, owasp top 10, rest api security, api vulnerabilities