Back to Learning Center
highOWASP API Top 10CWE-285CWE-639CWE-306

API Security

APIs form the backbone of modern applications, but their security is often overlooked. The OWASP API Security Top 10 highlights critical vulnerabilities that affect both REST and GraphQL APIs. Understanding these risks is essential for building secure microservices and preventing data breaches.

OWASP API Security Top 10

The OWASP API Security Top 10 identifies the most critical risks:

  1. Broken Object Level Authorization (BOLA)
  2. Broken Authentication
  3. Broken Object Property Level Authorization
  4. Unrestricted Resource Consumption
  5. Broken Function Level Authorization
  6. Unrestricted Access to Sensitive Business Flows
  7. Server-Side Request Forgery
  8. Security Misconfiguration
  9. Improper Inventory Management
  10. Unsafe Consumption of APIs

Broken Object Level Authorization (BOLA)

BOLA (formerly IDOR) is the most critical API vulnerability. It occurs when APIs expose object IDs without validating that the requesting user has permission to access them.

Vulnerable REST API

javascript
// Vulnerable: No authorization check
app.get('/api/users/:id/orders', async (req, res) => {
  const { id } = req.params;
  
  // DANGEROUS: Just fetches orders without checking ownership
  const orders = await db.query(
    'SELECT * FROM orders WHERE user_id = ?',
    [id]
  );
  
  res.json(orders);
});

// Attack: Change user ID to access other users' orders
// GET /api/users/12345/orders  (attacker's ID: 99999)
// Returns another user's order history

Secure Implementation

javascript
// Secure: Verify ownership
app.get('/api/users/:id/orders', authenticate, async (req, res) => {
  const requestedUserId = req.params.id;
  const authenticatedUserId = req.user.id;
  
  // Check if user can access this resource
  if (requestedUserId !== authenticatedUserId && !req.user.isAdmin) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  const orders = await db.query(
    'SELECT * FROM orders WHERE user_id = ?',
    [requestedUserId]
  );
  
  res.json(orders);
});

// Even better: Don't expose IDs at all
app.get('/api/my/orders', authenticate, async (req, res) => {
  const orders = await db.query(
    'SELECT * FROM orders WHERE user_id = ?',
    [req.user.id]  // Always use authenticated user's ID
  );
  
  res.json(orders);
});

GraphQL-Specific Vulnerabilities

Introspection Exposure

GraphQL introspection reveals your entire API schema, including types, queries, mutations, and sometimes deprecated or hidden fields:

graphql
# Introspection query - reveals entire schema
query {
  __schema {
    types {
      name
      fields {
        name
        type { name }
      }
    }
  }
}

# Response exposes everything:
# - adminDeleteUser mutation (hidden from docs)
# - internalNotes field on User type
# - deprecatedPassword field (oops!)

Batching Attacks

graphql
# Batching attack - brute force in single request
query {
  a1: login(email: "admin@example.com", password: "password1") { token }
  a2: login(email: "admin@example.com", password: "password2") { token }
  a3: login(email: "admin@example.com", password: "password3") { token }
  # ... hundreds more attempts in one request
  a500: login(email: "admin@example.com", password: "password500") { token }
}

# Rate limiting per-request won't stop this

Nested Query DoS

graphql
# DoS via deeply nested query
query {
  user(id: "1") {
    friends {
      friends {
        friends {
          friends {
            friends {
              # N+1 query explosion
              # Each level multiplies database queries
              name
            }
          }
        }
      }
    }
  }
}

Mass Assignment

APIs that blindly accept all client-supplied data can allow attackers to modify fields they shouldn't have access to:

javascript
// Vulnerable: Accepts all fields from request body
app.put('/api/users/:id', authenticate, async (req, res) => {
  const { id } = req.params;
  
  // DANGEROUS: Spreads entire request body into update
  await db.query('UPDATE users SET ? WHERE id = ?', [req.body, id]);
  
  res.json({ success: true });
});

// Attack: Add isAdmin field to request
// PUT /api/users/123
// { "name": "hacker", "isAdmin": true, "role": "superuser" }

Prevention: Allowlist Fields

javascript
// Secure: Explicitly allowlist fields
const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'avatar'];

app.put('/api/users/:id', authenticate, async (req, res) => {
  const { id } = req.params;
  
  // Only pick allowed fields
  const updates = {};
  for (const field of ALLOWED_UPDATE_FIELDS) {
    if (req.body[field] !== undefined) {
      updates[field] = req.body[field];
    }
  }
  
  if (Object.keys(updates).length === 0) {
    return res.status(400).json({ error: 'No valid fields to update' });
  }
  
  await db.query('UPDATE users SET ? WHERE id = ?', [updates, id]);
  res.json({ success: true });
});

Rate Limiting and Resource Protection

javascript
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';

// Per-user rate limiting
const apiLimiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  keyGenerator: (req) => req.user?.id || req.ip,
  message: { error: 'Too many requests' }
});

// Stricter limits for sensitive endpoints
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 5, // 5 attempts per hour
  keyGenerator: (req) => req.body.email || req.ip,
  message: { error: 'Too many login attempts' }
});

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

GraphQL Security

javascript
import { createServer } from '@graphql-yoga/node';
import { useDepthLimit } from '@graphql-yoga/plugin-depth-limit';
import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection';

const server = createServer({
  plugins: [
    // Disable introspection in production
    process.env.NODE_ENV === 'production' && useDisableIntrospection(),
    
    // Limit query depth to prevent DoS
    useDepthLimit({ maxDepth: 5 }),
  ].filter(Boolean),
  
  context: async ({ request }) => {
    return {
      user: await authenticateRequest(request),
    };
  },
});

// Resolver with authorization
const resolvers = {
  Query: {
    user: async (_, { id }, context) => {
      // Always check authorization in resolvers
      if (context.user.id !== id && !context.user.isAdmin) {
        throw new GraphQLError('Not authorized');
      }
      return db.users.findById(id);
    },
  },
};

Security Checklist

  • Implement object-level authorization on every endpoint
  • Use allowlists for mass assignment protection
  • Apply rate limiting per user and per endpoint
  • Disable GraphQL introspection in production
  • Limit GraphQL query depth and complexity
  • Validate all input with schemas (Zod, Joi)
  • Use proper HTTP methods (GET for reads, POST for mutations)
  • Return minimal data (avoid oversharing in responses)
  • Maintain API inventory and deprecate old versions
  • Log and monitor all API access patterns

Practice Challenges

View all

Related Articles

View all