API Security

OWASP API Security Top 10: Every Risk Explained with Examples

A deep dive into the OWASP API Security Top 10 2023 — each vulnerability explained with a real-world attack scenario and concrete remediation steps you can implement today.

October 5, 20258 min readShipSafer Team

OWASP's API Security Top 10 documents the most critical security risks facing modern APIs. The 2023 edition reflects years of bug bounty reports, penetration test findings, and incident analyses. Understanding these risks is essential for any team building or securing APIs — because attackers use this exact list as their attack playbook.

This guide walks through all 10 risks with real scenarios and practical remediations.

API1:2023 — Broken Object Level Authorization (BOLA)

BOLA (also called IDOR — Insecure Direct Object Reference) is the most common and impactful API vulnerability. It occurs when an API does not verify that the authenticated user is authorized to access the specific object they are requesting.

Attack Scenario:

GET /api/orders/12345
Authorization: Bearer <user_A_token>

If the API returns order 12345 regardless of whether it belongs to User A, any authenticated user can enumerate and access other users' orders by changing the ID.

Fix:

app.get('/api/orders/:orderId', authenticate, async (req, res) => {
  const order = await Order.findOne({
    orderId: req.params.orderId,
    userId: req.user.userId, // ALWAYS scope to the authenticated user
  });

  if (!order) return res.status(404).json({ error: 'Order not found' });
  res.json({ data: order });
});

Always filter database queries by the authenticated user's ID. Never trust object IDs from the request alone.

API2:2023 — Broken Authentication

Broken authentication covers a wide range of weaknesses: weak credentials policies, missing brute-force protection, insecure token storage, and missing multi-factor authentication.

Attack Scenario:

An API endpoint POST /api/auth/login accepts unlimited password guesses with no rate limiting. An attacker runs a credential stuffing attack with leaked username/password pairs from a breach database.

Fix:

import rateLimit from 'express-rate-limit';

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true, // Don't count successful logins
  keyGenerator: (req) => req.body.email ?? req.ip,
  message: { error: 'Too many failed login attempts. Try again in 15 minutes.' },
});

app.post('/api/auth/login', loginLimiter, async (req, res) => {
  // Constant-time comparison to prevent timing attacks
  const isValid = await bcrypt.compare(password, user.passwordHash);
  // ...
});

Use constant-time comparisons for all authentication checks. Store JWT tokens in HTTP-only cookies, not localStorage.

API3:2023 — Broken Object Property Level Authorization

Even when you correctly check that a user can access an object, you may return or allow modification of properties they shouldn't be able to see or change. This is also known as mass assignment or over-exposure.

Attack Scenario:

PATCH /api/users/me
Content-Type: application/json

{ "email": "new@email.com", "role": "admin" }

If the API blindly applies the request body to the user object, the attacker elevates their own privileges.

Fix:

import { z } from 'zod';

const UpdateProfileSchema = z.object({
  email: z.string().email().optional(),
  name: z.string().max(100).optional(),
  // 'role' and 'userId' are NOT in this schema — they cannot be changed here
});

app.patch('/api/users/me', authenticate, async (req, res) => {
  const result = UpdateProfileSchema.safeParse(req.body);
  if (!result.success) return res.status(400).json({ error: 'Invalid input' });

  await User.findOneAndUpdate(
    { userId: req.user.userId },
    result.data, // Only allowed fields
  );
});

Always use an explicit allowlist of modifiable fields. Never spread the entire request body into an update.

API4:2023 — Unrestricted Resource Consumption

APIs that allow clients to trigger expensive operations without limits are vulnerable to denial of service, either intentional or accidental.

Attack Scenario:

An API endpoint POST /api/report generates a PDF report from a date range. Without limits, an attacker requests a 10-year date range thousands of times, exhausting CPU and memory.

Fix:

const ReportSchema = z.object({
  startDate: z.string().datetime(),
  endDate: z.string().datetime(),
}).refine(data => {
  const start = new Date(data.startDate);
  const end = new Date(data.endDate);
  const diffDays = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
  return diffDays <= 90; // Max 90-day range
}, { message: 'Date range cannot exceed 90 days' });

Enforce:

  • Maximum page sizes for list endpoints (limit: max(1, min(limit, 100)))
  • Maximum date ranges for analytics queries
  • Maximum file sizes for uploads
  • Maximum number of items in bulk operations
  • Rate limits on computationally expensive endpoints

API5:2023 — Broken Function Level Authorization

While BOLA is about accessing another user's objects, Broken Function Level Authorization is about accessing functionality you shouldn't have — such as admin endpoints.

Attack Scenario:

DELETE /api/admin/users/user_789
Authorization: Bearer <regular_user_token>

If the API only checks that the user is authenticated (not that they are an admin), any user can call admin endpoints.

Fix:

function requireRole(role: 'admin' | 'moderator') {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!req.user) return res.status(401).json({ error: 'Unauthorized' });
    if (req.user.role !== role) return res.status(403).json({ error: 'Forbidden' });
    next();
  };
}

app.delete('/api/admin/users/:userId',
  authenticate,
  requireRole('admin'),
  async (req, res) => {
    // Only admins reach here
  }
);

Do not rely on obscurity ("the admin endpoint is hidden so users won't find it"). Every endpoint must enforce its own authorization, regardless of whether it appears in the public documentation.

API6:2023 — Unrestricted Access to Sensitive Business Flows

This risk covers APIs that allow automation of business processes in ways that cause harm — such as bulk ticket purchasing, loyalty points farming, or automated account creation for spam.

Attack Scenario:

A concert ticketing API has POST /api/checkout with no rate limiting. A bot purchases all available tickets within seconds of release, then resells them.

Fix:

// CAPTCHA for checkout
const checkoutLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 3, // 3 purchases per minute per user
  keyGenerator: (req) => req.user.userId,
});

// Bot detection via behavior analysis
app.post('/api/checkout', authenticate, checkoutLimiter, async (req, res) => {
  // Verify CAPTCHA token
  const captchaValid = await verifyCaptcha(req.body.captchaToken);
  if (!captchaValid) return res.status(400).json({ error: 'CAPTCHA required' });

  // Device fingerprinting
  // Email verification requirement for new accounts
  // Limit items per transaction
});

API7:2023 — Server-Side Request Forgery (SSRF)

SSRF occurs when an API fetches a URL provided by the client, allowing attackers to reach internal services.

Attack Scenario:

POST /api/webhooks/preview
{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/role" }

On AWS, this returns the EC2 instance's IAM credentials.

Fix:

import { URL } from 'url';
import dns from 'dns/promises';

async function isSafeUrl(rawUrl: string): Promise<boolean> {
  try {
    const parsed = new URL(rawUrl);
    if (!['http:', 'https:'].includes(parsed.protocol)) return false;

    const addresses = await dns.lookup(parsed.hostname, { all: true });
    const privateRanges = [/^10\./, /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./, /^127\./, /^169\.254\./];

    return !addresses.some(({ address }) =>
      privateRanges.some(pattern => pattern.test(address))
    );
  } catch {
    return false;
  }
}

API8:2023 — Security Misconfiguration

Security misconfiguration covers a broad set of issues: open S3 buckets, default credentials, verbose error messages, unnecessary HTTP methods, permissive CORS, and missing security headers.

Common Examples:

// WRONG: CORS allows everything
app.use(cors({ origin: '*', credentials: true }));

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

// WRONG: Debug mode in production
app.set('env', 'development'); // Enables stack traces in Express responses

Fix: Use security headers (Helmet.js), restrict CORS to known origins, return generic 500 errors, and disable debug modes. Run a security scanner on every deployment to catch new misconfigurations.

API9:2023 — Improper Inventory Management

Shadow APIs — endpoints that exist but are undocumented, unmaintained, or forgotten — are a major risk. These often run older, unpatched versions of your API.

Fix:

  • Maintain an API inventory (OpenAPI/Swagger) and treat it as code.
  • Scan your infrastructure for unlisted API endpoints with tools like Postman's API catalog or your API gateway logs.
  • Sunset old versions with clear timelines.
  • Add monitoring and alerting on all API endpoints, not just documented ones.
# openapi.yaml — document everything
paths:
  /api/v1/users/{userId}:
    get:
      security:
        - bearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
            format: uuid

API10:2023 — Unsafe Consumption of APIs

When your API consumes external APIs or webhooks, those third parties become part of your attack surface. If a third-party API is compromised or returns malicious data, your system should not be vulnerable.

Attack Scenario:

Your application fetches user profile data from a third-party OAuth provider and renders it. The provider's API has been compromised, and a user's displayName now contains <script>alert(1)</script>. If you render this without sanitization, you have a stored XSS.

Fix:

import DOMPurify from 'isomorphic-dompurify';
import { z } from 'zod';

// Validate third-party API responses with Zod
const ExternalUserSchema = z.object({
  id: z.string().max(100),
  displayName: z.string().max(200),
  email: z.string().email(),
});

async function fetchExternalUser(token: string) {
  const response = await fetch('https://api.provider.com/user', {
    headers: { Authorization: `Bearer ${token}` },
  });

  const raw = await response.json();

  // Validate the shape and types
  const result = ExternalUserSchema.safeParse(raw);
  if (!result.success) throw new Error('External API returned unexpected data');

  // Sanitize string fields before storage
  return {
    ...result.data,
    displayName: DOMPurify.sanitize(result.data.displayName, { ALLOWED_TAGS: [] }),
  };
}

Always validate and sanitize data from third-party APIs just as you would user input — because they may be compromised.

Building a Defense-In-Depth Strategy

The OWASP API Security Top 10 is not a checklist to check off once — it's a framework for ongoing security thinking. The most effective approach is defense in depth:

  1. Authentication — Verify identity on every request.
  2. Authorization — Verify permission for every resource and action.
  3. Input validation — Reject malformed or out-of-range inputs.
  4. Rate limiting — Prevent abuse of any single endpoint.
  5. Monitoring — Alert on anomalous patterns.
  6. Inventory — Know every API endpoint you expose.

Integrating automated API security scanning into your CI/CD pipeline catches regressions before they reach production, complementing the manual design-time thinking this list encourages.

owasp
api security
vulnerabilities
api security top 10
security testing

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.