API Security Best Practices Mapped to the OWASP Top 10

API security best practices for each OWASP API Security Top 10 2023 risk, plus how to declare auth, scopes and 401/403 responses in OpenAPI.

On this page

The API security best practices that matter most follow the OWASP API Security Top 10 2023: check that the caller owns every object it asks for, validate tokens properly, return only the properties the caller may see, limit how much work one client can trigger, and check permissions per function, not only per login. Some of these controls are visible in an OpenAPI spec, through securitySchemes, per-operation scopes and 401 and 403 responses. The most common failures, object and property authorization, live in your handler code, so a spec can document them but never prove them. The table below maps each risk to its control and shows where, if anywhere, the spec reflects it.

The OWASP API Top 10 2023, risk by risk

OWASP 2023 riskMain controlVisible in OpenAPI?
API1 Broken Object Level AuthorizationCheck ownership of every object ID in the data queryPartly: 403 or 404 responses, uuid IDs
API2 Broken AuthenticationStrict token validation, no credentials in URLsYes: securitySchemes and security
API3 Broken Object Property Level AuthorizationExplicit response schemas, readOnly fields, no mass assignmentYes: schemas, readOnly, additionalProperties
API4 Unrestricted Resource ConsumptionRate limits, page size caps, upload limits, timeoutsPartly: 429 responses, maximum on limit parameters
API5 Broken Function Level AuthorizationPer-operation permissions, deny by defaultYes: scopes per operation
API6 Unrestricted Access to Sensitive Business FlowsPer-flow limits and abuse detection on purchases, sign-ups, messagesNo
API7 Server Side Request ForgeryAllowlist outbound hosts, block private rangesPartly: format: uri on URL inputs
API8 Security MisconfigurationHTTPS only, tight CORS, generic error bodiesPartly: https server URLs
API9 Improper Inventory ManagementEvery deployed host and version in the spec, old ones retiredYes: servers and paths are the inventory
API10 Unsafe Consumption of APIsValidate third-party responses like user inputNo, unless you keep specs for the APIs you call
Risk names from the OWASP API Security Top 10 2023.

Declare authentication with OpenAPI securitySchemes

An undeclared auth scheme is a security problem in its own right: client generators cannot add the header, testers guess, and reviewers cannot see which operations are public. OpenAPI 3.x defines five scheme types under components.securitySchemes: apiKey (with name and in, where in is query, header or cookie), http (with scheme such as bearer or basic, and an optional bearerFormat), oauth2 (with flows and scopes), openIdConnect (with openIdConnectUrl), and, from OpenAPI 3.1, mutualTLS.

The security requirement rules are easy to get wrong. A security array lists alternatives, and the client needs to satisfy only one of them. Several schemes inside one requirement object must all be satisfied. An empty object {} in the array makes auth optional. A security array on an operation overrides the top-level one, and an empty array security: [] removes auth for that operation. That last rule is how a health check becomes public, and also how an endpoint becomes public by accident, so search every spec change for security: [] during review.

YAMLinvoices-api.yaml
openapi: 3.0.3
info:
  title: Invoices API
  version: 1.0.0
servers:
  - url: https://api.example.com
security:
  - OAuth2:
      - invoices:read
paths:
  /v1/health:
    get:
      summary: Liveness check
      security: []
      responses:
        "200":
          description: Service is up
  /v1/invoices/{id}:
    get:
      summary: Get one invoice owned by the caller
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The invoice
        "401":
          description: Missing or invalid access token
        "403":
          description: Token lacks the invoices:read scope
        "404":
          description: No invoice with this id belongs to the caller
  /v1/invoices/{id}/refunds:
    post:
      summary: Refund an invoice
      security:
        - OAuth2:
            - invoices:write
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "201":
          description: Refund created
        "401":
          description: Missing or invalid access token
        "403":
          description: Token lacks the invoices:write scope
components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.example.com/authorize
          tokenUrl: https://auth.example.com/token
          scopes:
            invoices:read: Read invoices
            invoices:write: Create refunds
Top-level security requires invoices:read; the refund operation overrides it with invoices:write; the health check opts out with security: [].

Three details in this spec carry the security design. The refund operation asks for its own scope, so a read-only token cannot move money (API5). Every protected operation documents 401 and 403, which tells client authors which failure means "log in again" and which means "you are not allowed". The ID is a uuid, which OWASP recommends for record IDs because sequential numbers are easy to guess, though an unguessable ID never replaces an ownership check. For a minimal bearer-token version of the same pattern, see the OpenAPI authentication example.

API1: check ownership in the query, not after it

Broken object level authorization is first on the OWASP list because every endpoint that takes an ID from the client is a candidate. One of OWASP's scenarios is a store platform where /shops/{shopName}/revenue_data.json returned any shop's revenue to any logged-in user once the attacker learned the URL pattern. The fix is to make ownership part of the lookup itself, so a missing check cannot slip in later.

AvoidAuthenticated, but not authorized

JavaScript
app.get("/v1/invoices/:id", requireAuth, async (req, res) => {
  const invoice = await db.invoices.findById(req.params.id);
  if (!invoice) return res.status(404).end();
  res.json(invoice);
});

Any valid token can read any invoice by changing the ID.

PreferOwnership is part of the lookup

JavaScript
app.get("/v1/invoices/:id", requireAuth, async (req, res) => {
  const invoice = await db.invoices.findOne({
    id: req.params.id,
    tenantId: req.auth.tenantId,
  });
  if (!invoice) return res.status(404).end();
  res.json(toPublicInvoice(invoice));
});

Another tenant's invoice returns the same 404 as a missing one, so the response does not confirm that the ID exists. toPublicInvoice picks the fields this caller may see (API3).

Returning 404 rather than 403 for someone else's object is a design choice: it hides whether the ID exists. Document it in the spec, as the 404 description above does, so client developers do not file it as a bug.

API2: authentication and token handling

  • Send tokens in the Authorization header. RFC 6750 says bearer tokens should not be passed in page URLs, because browsers, servers and proxies do not protect URLs in history and logs. The same reasoning applies to an apiKey scheme with in: query.
  • Validate every token on every request: signature with an allowlist of algorithms, issuer, audience, and expiry. Fetch signing keys from the issuer rather than hard-coding them, so key rotation does not need a deploy.
  • Use the RFC 6750 error codes: invalid_token with 401 for expired or malformed tokens, insufficient_scope with 403 when the token is valid but lacks a scope. RFC 9110 requires a WWW-Authenticate header on every 401.
  • Rate-limit login, token and password-reset endpoints separately and more strictly than the rest of the API.

API3 and API5: properties and functions

API3 covers two mirror-image bugs. Excessive data exposure is a response that includes fields the caller should not see, usually because a handler serializes the whole database row. Mass assignment is a request that sets fields the caller should not control, such as role or balance, because the handler copies the whole body into the model. In the spec, give responses explicit properties, mark server-owned fields readOnly so they are not accepted on writes, and use separate request and response schemas when they differ. Then test that the server keeps the promise: validating responses against the schema with additionalProperties set to false catches a leaked field the first time it appears.

API5 is the same idea one level up: a regular user calling an admin function. Give each operation the narrowest OAuth scope it needs, as the refund operation does, deny by default in the gateway or middleware, and do not rely on admin routes being unlisted. Listing them in an internal spec is better than hiding them, because an unlisted route is also an unreviewed one.

API4 and API6: limit the work one client can cause

OWASP lists the limits to set for API4: execution timeouts, memory, file descriptors, processes, upload size, operations per request, records per page, and spending limits on paid third-party services. Several of these belong in the spec: a maximum on page size parameters, a maxLength on strings, and a documented 429 response. The API rate limiting guide covers algorithms and the headers that tell clients their quota. API6 needs limits per business flow rather than per request, such as how many accounts one device can create in an hour. Those rules depend on your product, and no spec field expresses them.

API7 to API10: outbound calls, configuration, inventory

  • API7 SSRF: when the API fetches a URL the client supplies, such as a webhook target or an image import, allowlist hosts and schemes, resolve the address, and reject private and loopback ranges before connecting.
  • API8 misconfiguration: list only https server URLs, set CORS to known origins, and keep stack traces out of error bodies.
  • API9 inventory: treat the spec as the list of what is deployed. Every host in servers and every version in paths should match production, and a retired version should be removed from both.
  • API10 unsafe consumption: validate responses from third-party APIs against their schema before you use them, with the same care as user input.

What tools can and cannot check

A design score is the wrong instrument for security. The spec above scores 95.6 on APIForge's deterministic score, losing points for a health check with no error response and a 4-segment refund path. We then deleted every security block and the securitySchemes component, and the score stayed at 95.6, because none of its seven categories reads auth. Use the OpenAPI validator to catch structural errors in the spec, and review the security sections by hand.

For runtime checks, the API testing workbench reads the operation's security requirement, fills in the API key name and location from the matching scheme, and sends requests with bearer, basic or API key auth. That makes a manual BOLA test quick: sign in as user A, request an invoice ID that belongs to user B, and confirm the API returns 404. Repeat with a token that lacks invoices:write against the refund operation and confirm 403. If Swagger UI sends no token after you click Authorize, Swagger UI Authorize not sending the bearer token shows nine spec setups and the header each one produced.

Frequently asked questions

Broken object level authorization is API1 in the OWASP API Security Top 10 2023. It happens when an API checks that the caller is logged in but not that the caller may access the specific object ID in the request. Fix it by including the owner or tenant in the database lookup.

Return 401 when the token is missing, expired or invalid, with a WWW-Authenticate header as RFC 9110 requires. Return 403 when the token is valid but lacks permission, such as a missing scope. Some APIs return 404 for other users' objects so they do not confirm the ID exists.

Set security: [] on that operation. An operation-level security array overrides the top-level one, and an empty array removes the requirement. Review every security: [] in a spec change, because the same line can make an endpoint public by mistake.

Avoid it. RFC 6750 says bearer tokens should not go in page URLs because URLs end up in browser history and server logs, and API keys have the same exposure. Use a header, and declare it as an apiKey scheme with in: header.

Sources

  1. OWASP API Security Top 10 2023OWASP
  2. OWASP API1:2023 Broken Object Level AuthorizationOWASP
  3. OpenAPI Specification 3.1.0: Security Scheme ObjectOpenAPI Initiative
  4. RFC 6750: OAuth 2.0 Bearer Token UsageIETF