Skip to content
Back to Tutorials

How to Audit and Lock Down APIs Using the OWASP API Security Top 10

Intermediate · 1 hour 10 minutes · 15 min read · Byte Smith ·

Before you begin

  • An API inventory or at least access to source repositories, API gateway configs, and service documentation
  • Authentication and authorization model documentation for the APIs you want to review
  • A test or staging environment where you can safely replay requests
  • Access logs, metrics, or gateway telemetry for the APIs under review
  • Basic access to source code or OpenAPI specifications

What you'll learn

  • Build or verify a real API inventory that includes public, internal, partner, and shadow APIs
  • Audit object-level, function-level, and tenant-level authorization more effectively
  • Review input validation, schema enforcement, and mass-assignment-style risks
  • Check rate limits, abuse controls, and cost amplification paths
  • Find sensitive data exposure in responses, logs, debug routes, and third-party calls
  • Turn audit findings into a prioritized remediation plan your team can actually execute
1
2
3
4
5
6
7
8
9
On this page

API security is still one of the fastest ways to get breached because APIs expose business functions, object identifiers, privileged actions, and sensitive data in a way that is easy to automate at scale. A small authorization flaw on one endpoint can often be replayed across thousands of IDs, tenants, or objects. That is why an API review should not stop at “we use OAuth” or “the gateway has auth enabled.”

Modern AI and SaaS integrations make the problem worse. Many teams now consume third-party APIs as if they were trusted internal systems, proxy AI prompts and results through API layers, and add partner-specific or beta endpoints faster than inventory can keep up. That combination creates exactly the kinds of problems OWASP highlights in improper inventory management, unrestricted resource consumption, and unsafe consumption of APIs.

This tutorial gives you a practical audit workflow based on the OWASP API Security Top 10. You will build a usable API inventory, review authorization and tenant boundaries, inspect schema and abuse controls, check for data exposure, review operational security, and then convert the findings into a remediation plan that fixes the biggest risks first.

Step 1: Build or verify your API inventory

Do not start with vulnerability scanning. Start with visibility. If you do not know which APIs exist, who owns them, which version is live, and who can reach them, you are already in API9 territory.

Inventory public, internal, partner, and shadow APIs

Build a single inventory file that answers these questions for every API surface:

  • What is the hostname or base path?
  • Is it public, internal, partner-only, or unknown?
  • Who owns it?
  • Which auth model does it use?
  • What environment is it in?
  • Is there a documented deprecation plan?

File: api-inventory.csv

service,base_url,audience,owner,environment,auth,openapi_spec,current_version,status
payments-api,https://api.example.com/payments,public,payments-team,production,oauth2,openapi/payments.yaml,v2,active
admin-api,https://admin-api.internal.example.com,internal,platform-team,production,jwt-rbac,openapi/admin.yaml,v1,active
partner-orders,https://partners.example.com/orders,partner,bizops-team,production,api-key-plus-hmac,openapi/partner-orders.yaml,v1,active
legacy-beta,https://beta-api.example.com,unknown,unknown,production,unknown,missing,v0,investigate

Pull inventory from code and traffic, not just docs

Documentation is a starting point, not the truth. To find undocumented or stale endpoints, combine source search and access logs.

Example source search:

grep -R "router\.\|app\.\|@RequestMapping\|@GetMapping\|@PostMapping" ./services
grep -R "openapi\|swagger" ./services

Example log-derived inventory from JSON access logs:

jq -r '.host + "," + .method + "," + .path' access.log \
  | sort -u \
  | tee observed-routes.csv

If your gateway logs route IDs or upstream service names, extract those too.

Mark shadow and stale APIs explicitly

A real audit needs a bucket for endpoints you do not fully understand yet. Do not force them into “internal” or “safe” just to make the spreadsheet look cleaner.

Create a review worksheet:

File: shadow-api-review.md

# Shadow API Review

## beta-api.example.com
- Owner: unknown
- Environment: production
- OpenAPI spec: missing
- Auth model: unknown
- Last seen in traffic: 2026-03-08
- Public DNS: yes
- Action: classify or remove within 7 days

Inventory versions and data flows

Do not inventory only hostnames. Track versions and data-sharing paths too.

File: api-data-flows.yaml

payments-api:
  inbound_clients:
    - web-app
    - mobile-app
  outbound_dependencies:
    - tax-provider
    - fraud-provider
  sensitive_data:
    - customer-name
    - partial-card-metadata
    - billing-address

partner-orders:
  inbound_clients:
    - reseller-a
    - reseller-b
  outbound_dependencies:
    - warehouse-api
  sensitive_data:
    - customer-email
    - shipping-address
Tip
If you cannot quickly answer “which APIs are public, which are partner-only, and which versions are still live,” your first remediation is inventory, not penetration testing.

You should now have a working API inventory with real routes, audiences, versions, owners, and data flows.

Step 2: Review auth and authorization

This is the highest-yield part of the audit. The biggest API breaches usually come from missing or inconsistent authorization checks, not from exotic payloads.

Check object-level authorization

Object-level authorization asks: can this caller access this specific object?

If your API loads a record from a user-supplied ID, UUID, slug, or foreign key, you need an ownership or policy check after lookup and before returning or mutating the object.

File: src/middleware/ownership.ts

import { Request, Response, NextFunction } from "express";

export function requireAccountOwnership(
  fetchAccount: (accountId: string) => Promise<{ id: string; tenantId: string } | null>
) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const accountId = req.params.accountId;
    const account = await fetchAccount(accountId);

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

    if (account.tenantId !== req.auth?.tenantId) {
      return res.status(403).json({ error: "Forbidden" });
    }

    res.locals.account = account;
    next();
  };
}

If your code path looks like “read ID from path, fetch object, return object,” but never checks tenant or ownership, that is a red flag.

Check function-level authorization

Function-level authorization asks: can this caller access this function or action?

That includes admin routes, exports, bulk actions, approval flows, and sensitive business operations.

File: src/middleware/requireScope.ts

import { Request, Response, NextFunction } from "express";

export function requireScope(requiredScope: string) {
  return (req: Request, res: Response, next: NextFunction) => {
    const scopes = req.auth?.scopes ?? [];

    if (!scopes.includes(requiredScope)) {
      return res.status(403).json({ error: "Missing required scope" });
    }

    next();
  };
}

And then use it consistently:

app.post(
  "/admin/users/:userId/suspend",
  requireScope("admin:user:suspend"),
  suspendUserHandler
);

Check tenant separation

Multi-tenant APIs should not rely only on a client-supplied tenant ID. The tenant context should come from verified auth claims or server-side lookup, not from an arbitrary header the client can change.

Bad pattern:

const tenantId = req.headers["x-tenant-id"];

Better pattern:

const tenantId = req.auth?.tenantId;

Then verify every data access path constrains by that tenant.

Check token scope, audience, and expiry

For each auth mechanism, answer:

  • Does the token have an audience check?
  • Are scopes or roles narrow enough?
  • Is expiry reasonable?
  • Are refresh tokens handled separately?
  • Can one token access both public and admin APIs?

Create a simple auth matrix:

File: auth-matrix.yaml

payments-api:
  auth_type: oauth2-jwt
  audience: payments-api
  scopes:
    - payments:read
    - payments:write
  admin_scopes:
    - payments:refund
  max_access_token_lifetime_minutes: 15

admin-api:
  auth_type: internal-jwt
  audience: admin-api
  scopes:
    - admin:users:read
    - admin:users:suspend
  max_access_token_lifetime_minutes: 10
Warning
“Authenticated” is not the same as “authorized.” For API audits, assume every path parameter, every object ID, and every action endpoint needs its own authorization decision.

You should now know whether your APIs enforce ownership, action-level permissions, tenant boundaries, and sensible token scope rules.

Step 3: Review input handling and schema controls

This step is where you find mass-assignment-style bugs, weak query validation, unsafe parsing, and schema drift between docs and actual behavior.

Validate request bodies against a schema

Do not rely on hand-written if statements spread across controllers. Use explicit request schemas for body, path params, and query params.

File: src/validation/updateProfile.ts

import { z } from "zod";

export const updateProfileSchema = z.object({
  displayName: z.string().min(1).max(80),
  marketingOptIn: z.boolean().optional(),
});

File: src/middleware/validateBody.ts

import { AnyZodObject } from "zod";
import { Request, Response, NextFunction } from "express";

export function validateBody(schema: AnyZodObject) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);

    if (!result.success) {
      return res.status(400).json({
        error: "Invalid request body",
        details: result.error.flatten(),
      });
    }

    req.body = result.data;
    next();
  };
}

Prevent object-property overreach

Only allow known properties through. Do not bind arbitrary JSON into ORM models or update objects directly.

Bad pattern:

await userRepository.update(req.params.userId, req.body);

Better pattern:

const allowed = {
  displayName: req.body.displayName,
  marketingOptIn: req.body.marketingOptIn,
};

await userRepository.update(req.params.userId, allowed);

Validate query parameters and pagination

Query parameters are part of the attack surface too. Enforce ranges and defaults.

File: src/validation/listOrdersQuery.ts

import { z } from "zod";

export const listOrdersQuerySchema = z.object({
  limit: z.coerce.number().int().min(1).max(100).default(25),
  offset: z.coerce.number().int().min(0).default(0),
  status: z.enum(["pending", "paid", "failed"]).optional(),
});

Review deserialization and parser behavior

Flag any endpoint that:

  • accepts raw serialized objects without schema validation
  • dynamically interprets operators or field names from JSON
  • accepts GraphQL queries with weak complexity controls
  • accepts unsafe file formats or document transforms

Align OpenAPI with live behavior

If your OpenAPI spec says only displayName is writable, but the endpoint actually accepts isAdmin, tenantId, or accountBalance, you have a spec-to-runtime gap.

A safe request schema excerpt looks like this:

File: openapi/profile.yaml

paths:
  /v1/me/profile:
    patch:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                displayName:
                  type: string
                  maxLength: 80
                marketingOptIn:
                  type: boolean
Note
additionalProperties: false is one of the easiest ways to reduce accidental property-level exposure when your tooling and runtime support it.

You should now know whether your API enforces request schemas, limits writable properties, constrains query parameters, and keeps runtime behavior aligned with the documented contract.

Step 4: Review rate limiting and abuse controls

This is where you review both classic API abuse and modern cost-amplification paths. Not every abuse case is brute force. Some are pagination abuse, bulk export abuse, OTP floods, AI token spend abuse, or partner workflow automation.

Create route-level rate profiles

Do not apply one global limit to everything. Different endpoints need different controls.

File: rate-limit-profile.yaml

routes:
  /v1/auth/login:
    limit_per_minute: 5
    burst: 2
    key: ip_plus_username
  /v1/password/reset:
    limit_per_hour: 3
    key: account_identifier
  /v1/orders/search:
    limit_per_minute: 30
    burst: 10
    key: token_subject
  /v1/ai/generate:
    limit_per_minute: 10
    burst: 2
    key: tenant_id

Add implementation-level limits

File: src/middleware/rateLimit.ts

import rateLimit from "express-rate-limit";

export const loginRateLimit = rateLimit({
  windowMs: 60 * 1000,
  max: 5,
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => `${req.ip}:${req.body.username ?? "unknown"}`,
});

Review enumeration protection

Check whether attackers can cheaply enumerate:

  • user IDs
  • account IDs
  • invoice IDs
  • email existence
  • password reset flows
  • invite tokens
  • partner records

Look for inconsistent 404 vs 403 responses, timing differences, and different error bodies for “exists” versus “does not exist.”

Review bot and business-flow abuse

Identify flows with high business value:

  • coupon creation
  • trial creation
  • gift card redemption
  • bulk downloads
  • seat assignment
  • AI generation endpoints
  • OTP or email send endpoints

Add controls like:

  • route-specific quotas
  • idempotency keys
  • CAPTCHA or challenge on public flows
  • workflow anomaly alerts
  • queueing for high-cost async jobs

Review cost amplification paths

Modern APIs often proxy external costs:

  • SMS
  • email
  • document rendering
  • AI inference
  • geocoding
  • fraud checks

If a route can burn money per request, it needs stricter controls than a normal read-only endpoint.

Warning
Abuse controls are not just about availability. They are also about protecting money, partner quotas, and expensive downstream systems.

You should now know which endpoints need stronger burst controls, enumeration resistance, and cost-aware quotas.

Step 5: Review sensitive data exposure

This step catches APIs that technically require auth but still expose too much data to the wrong caller, the wrong log sink, or the wrong downstream service.

Audit response field exposure

For each sensitive resource, define a response allowlist by role or audience.

File: src/serializers/customerSerializer.ts

type Customer = {
  id: string;
  email: string;
  fullName: string;
  internalRiskScore: number;
  billingAddress: string;
};

export function serializeCustomerForSelf(customer: Customer) {
  return {
    id: customer.id,
    email: customer.email,
    fullName: customer.fullName,
    billingAddress: customer.billingAddress,
  };
}

export function serializeCustomerForSupport(customer: Customer) {
  return {
    id: customer.id,
    email: customer.email,
    fullName: customer.fullName,
  };
}

Do not return full ORM entities directly.

Redact logs

Access logs and app logs often leak tokens, emails, account numbers, or full request bodies. Redact before writing.

File: src/logging/redact.ts

export function redact(value: string): string {
  return value
    .replace(/Bearer\s+[A-Za-z0-9._-]+/g, "Bearer [REDACTED]")
    .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[REDACTED_EMAIL]");
}

And never log sensitive bodies by default on auth, payment, or AI prompt routes.

Remove or lock down debug endpoints

Review all routes like:

  • /debug
  • /internal
  • /health/details
  • /graphql-playground
  • /swagger
  • /metrics
  • /__admin

These are often left exposed because they are “for ops,” but production exposure must be intentional and protected.

Review third-party forwarding

If your API forwards prompts, payloads, customer data, or identifiers to external SaaS providers, document and validate that flow explicitly.

File: third-party-forwarding-review.yaml

dependency: external-ai-provider
forwarded_fields:
  - prompt
  - account_tier
  - locale
disallowed_fields:
  - raw_access_token
  - full_payment_card
  - national_id
response_validation: required
timeout_seconds: 15
Tip
Sensitive data exposure is often a serialization problem, not just an encryption problem. Review what each endpoint returns by role, not just whether the endpoint is authenticated.

You should now know whether your APIs over-return data, leak information to logs, expose debug surfaces, or forward more data than necessary to third parties.

Step 6: Review operational security

Good code-level controls still fail if the API edge is badly configured or if you cannot detect abuse in time.

Review gateway and reverse-proxy controls

For each API edge, check:

  • TLS everywhere it should be
  • only required HTTP methods exposed
  • body size limits
  • request timeout and upstream timeout policy
  • request ID propagation
  • schema validation where supported
  • IP allowlists for partner or admin APIs

Review monitoring and alerting

At minimum, track:

  • 401 and 403 spikes
  • 429 spikes
  • route-level latency
  • route-level 5xx
  • auth failures by endpoint
  • high-volume object ID sweeps
  • traffic to deprecated versions
  • unusual partner usage

Create a small metric checklist:

File: api-monitoring-checklist.md

# API Monitoring Checklist

- 401/403 dashboard per route
- 429 dashboard per route
- Token subject or tenant-based rate anomaly alerts
- Object enumeration detection on high-value routes
- Deprecated version traffic alerts
- Third-party dependency timeout alerts

Review versioning and deprecation

Every version should have:

  • an owner
  • an audience
  • an end-of-life date
  • a deprecation communication plan
  • a traffic retirement check

You can also expose deprecation and sunset headers.

File: src/middleware/deprecation.ts

import { Request, Response, NextFunction } from "express";

export function markDeprecated(res: Response) {
  res.setHeader("Deprecation", "true");
  res.setHeader("Sunset", "Wed, 30 Sep 2026 23:59:59 GMT");
  res.setHeader("Link", '</docs/migrate-v2>; rel="deprecation"');
}

Review partner and internal APIs too

Many orgs do a decent job on public APIs and a weak job on “internal only” APIs. Internal, partner, and admin routes should still be inventoried, versioned, monitored, and reviewed.

Info
A hidden API with weak monitoring is often more dangerous than a public API with strong controls, because nobody is paying attention to it.

You should now know whether the API edge is hardened, whether you can detect abuse, and whether old versions and hidden routes are being managed deliberately.

Step 7: Turn findings into a remediation plan

An API audit only matters if it produces changes. The best remediation plans separate fast risk reduction from design work that needs more time.

Capture findings in a structured format

File: api-audit-findings.yaml

findings:
  - id: API-001
    category: API1-BOLA
    service: payments-api
    endpoint: GET /v1/accounts/{accountId}
    severity: critical
    issue: Account lookup does not verify tenant ownership
    quick_fix: Add tenant-scoped query and authorization middleware
    owner: payments-team
    due_in_days: 7

  - id: API-002
    category: API3-BOPLA
    service: partner-orders
    endpoint: PATCH /v1/orders/{id}
    severity: high
    issue: Writable object accepts undocumented internal fields
    quick_fix: Add request schema with explicit allowlist
    owner: bizops-team
    due_in_days: 14

  - id: API-003
    category: API9-Inventory
    service: legacy-beta
    endpoint: unknown
    severity: high
    issue: Public beta host has no owner, no spec, and no retirement plan
    quick_fix: Remove public exposure or assign owner immediately
    owner: platform-team
    due_in_days: 3

Separate quick fixes from architectural fixes

Good quick fixes:

  • add ownership checks
  • add missing scope checks
  • restrict response fields
  • add schema validation
  • lower route limits
  • disable debug endpoints
  • remove stale public DNS or routing

Medium-term architectural fixes:

  • move authz into a centralized policy layer
  • split admin and public APIs
  • redesign tenant isolation
  • create automated inventory discovery
  • add admission or gateway policy controls
  • standardize response serializers and schema generation

Assign owners and due dates

Every finding needs:

  • one owning team
  • one severity
  • one remediation approach
  • one target date
  • one validation method

Re-test the fix

A fix is not complete until you can prove the abuse path is gone. For each finding, save one positive and one negative test.

File: tests/account-ownership.http

### Allowed: own tenant account
GET https://api.example.com/v1/accounts/acc_123
Authorization: Bearer {{tenant_a_token}}

### Blocked: another tenant account
GET https://api.example.com/v1/accounts/acc_999
Authorization: Bearer {{tenant_a_token}}

Common Setup Problems

Auditing only public APIs

Teams often review the public gateway and ignore internal, admin, partner, beta, and deprecated surfaces. That leaves some of the highest-risk routes outside the audit.

Assuming authentication equals authorization

A valid token is not proof that the caller should access a specific object, field, or function. Review authz separately at object, field, function, and tenant levels.

Finding shadow APIs but not removing them

Inventory work only reduces risk if stale or unknown APIs get ownership, protection, or deletion. A spreadsheet alone does not close exposure.

Generating OpenAPI specs but not enforcing them

A spec that is not tied to validation, tests, or review can drift until it becomes decoration. Prefer schema validation and contract tests over documentation-only specs.

Fixing one endpoint instead of the pattern

If you find one broken object-level authorization issue, search for the same access pattern across the codebase. API flaws often repeat by framework, serializer, or repository pattern.

Wrap-Up

A strong API audit starts with inventory and ends with prioritized remediation. The highest-value review path is usually: verify all API surfaces, check object and function authorization, constrain schemas and writable properties, lock down abuse controls, reduce sensitive data exposure, and then clean up operational gaps like hidden versions and weak monitoring.

Repeat this audit on a schedule, not just after incidents. A practical cadence is quarterly for high-risk APIs, after major auth or gateway changes, and whenever you add new partner, SaaS, or AI-dependent integrations. To keep inventory current, tie route discovery to code review, gateway config changes, and traffic telemetry instead of relying on documentation updates alone.

The main goal is not to “pass OWASP.” It is to make sure your APIs are hard to enumerate, hard to abuse, hard to over-consume, and easy to understand and retire when they should no longer exist.