API Security Architecture: OWASP API Top 10, BOLA, WAF Rules and Bot Mitigation (2026)

API Security Architecture: OWASP API Top 10, BOLA, WAF Rules and Bot Mitigation (2026)

API Security: OWASP API Top 10, BOLA, WAF and Bot Mitigation Architecture (2026)

A request can pass every control you bought and still be the breach. The token is valid, the JSON is well formed, the payload contains no SQL fragment, the client IP has a clean reputation, and the rate is polite. The only thing wrong is that the caller asked for /orders/1043 and order 1043 belongs to someone else. No signature in a web application firewall will ever flag that request, because nothing in it is malicious on its face.

That single observation explains why API security is a different discipline from classic web application security, and why the OWASP API Security Top 10 puts Broken Object Level Authorization at number one. The dominant failures are about who may do what to which object, and those are decisions only your application, or a policy engine that understands your data model, can make. Perimeter products still matter, but they answer a different question.

This post lays out a layered reference architecture that gives each control the job it can actually do. You will leave with a map of the 2023 risk list to controls, walk-throughs of the three authorization attacks, an honest account of what WAF rules and anomaly scoring can and cannot catch, and runnable examples: an OpenAPI schema that enforces a positive security model and a Python test that hunts for BOLA.

What this covers: the OWASP API Top 10 as a control map, a five-layer reference architecture, BOLA, BFLA and mass assignment attacks, WAF and Core Rule Set tuning, schema validation, gateway authorization with OPA, bot mitigation, rate limiting, inventory, detection, and failure modes.

Context and Background

The OWASP API Security Top 10 exists because the general OWASP Top 10 describes web pages and forms, while APIs expose the object model directly. The current edition, published in 2023, lists ten risks: API1 Broken Object Level Authorization, API2 Broken Authentication, API3 Broken Object Property Level Authorization, API4 Unrestricted Resource Consumption, API5 Broken Function Level Authorization, API6 Unrestricted Access to Sensitive Business Flows, API7 Server Side Request Forgery, API8 Security Misconfiguration, API9 Improper Inventory Management, and API10 Unsafe Consumption of APIs. I confirmed that list against the OWASP API Security Project edition page while writing this. The general web list was refreshed separately in 2025, but I did not find a newer API-specific edition, so the 2023 list is still the reference to design against.

Three changes in the 2023 edition shape the architecture below. Authorization was split into three levels: object (API1), property (API3, which merged the older excessive data exposure and mass assignment entries) and function (API5). Resource consumption was broadened from simple rate limits to anything that burns money or capacity, including SMS and email sends and third-party API calls. And API6 added a category that tooling had no vocabulary for: legitimate flows abused at scale, such as ticket scalping, reservation hoarding and referral-bonus farming.

Notice what the list is not. Only a minority of entries, mainly SSRF and part of misconfiguration, resemble the injection-style payloads that perimeter products have detected for two decades. The majority are logic flaws. The request is syntactically valid, the caller is authenticated, and the fault is a missing or wrong authorization check, a missing limit, or an endpoint nobody remembered to retire.

That is also why buying more of one control rarely helps. A WAF cannot learn that user 17 may not read user 18’s invoice. An identity provider proves who the caller is, not which rows they own. A rate limiter slows enumeration but does not stop it. Each layer is necessary, none is sufficient, and the architecture is mostly a question of assigning each risk to the layer with enough context to decide.

Two adjacent topics are covered elsewhere and referenced here rather than repeated. Token validation mistakes, the core of API2, are dissected in OAuth 2.1, PKCE and JWT validation pitfalls. The algorithms behind throttling are covered in distributed rate limiting with token bucket and GCRA. This post concentrates on how those pieces fit with authorization, WAF and bot controls into one design.

A Five-Layer Reference Architecture for API Security

A sound API security architecture assigns each OWASP risk to the cheapest layer that has enough context to decide it: the edge handles volume and known-bad traffic, the gateway enforces schema and coarse identity, a policy engine decides function-level access, and the service itself owns object-level authorization. Detection spans all layers.

API security reference architecture showing edge, gateway, policy, service and detection layers

Figure 1: Five-layer API security architecture. Each layer owns a class of OWASP API Top 10 risks; object-level checks stay in the service.

The diagram reads left to right along the request path. Layer 1 is the edge: a CDN or cloud WAF with DDoS absorption, TLS termination, IP and ASN reputation, and bot scoring. Layer 2 is the API gateway, which authenticates the token, validates the request against the OpenAPI contract, and applies per-consumer quotas. Layer 3 is a policy decision point, commonly Open Policy Agent, that answers “may this principal call this operation on this resource type?” Layer 4 is the service, which loads the object and checks ownership or tenancy. Layer 5 is the telemetry plane that every layer feeds.

Layer 1: the edge removes noise and volume

The edge exists to make everything downstream cheaper. Volumetric floods, scanner traffic, known exploit signatures and obviously automated clients are all better rejected before they consume gateway and service capacity. A managed WAF running the OWASP Core Rule Set is a reasonable default here, with the caveat explained in the WAF section below: it catches generic attack patterns, not authorization faults.

The edge is also the right place for coarse bot scoring and for geo or ASN rules that reflect real business constraints, such as an internal-only partner API that should never see residential IP space. Keep edge rules stateless and cheap. The edge does not know your users, so any rule that depends on “who owns this object” belongs further in.

Layer 2: the gateway enforces the contract

Gateways are where a positive security model becomes practical. Instead of listing what is forbidden, the gateway admits only what the OpenAPI document describes: known paths, known methods, declared parameters, declared content types, bounded body sizes, and schema-valid bodies. Everything else is rejected with a 4xx before application code runs. That single move neutralizes a large portion of mass assignment (API3), shadow endpoints (API9) and parser-abuse risk, because undeclared fields and undeclared routes simply do not exist as far as the gateway is concerned.

The gateway also performs authentication: signature, issuer, audience, expiry and, for sensitive operations, scope checks. It attaches verified claims as headers or metadata for downstream layers, and it applies per-consumer rate limits keyed on the authenticated identity rather than the IP address. If you are choosing the product for this layer, the trade-offs are compared in Kong vs Envoy Gateway vs Apache APISIX.

Layer 3 and 4: policy for functions, the service for objects

Function-level authorization (API5) is about operations: can a standard user call DELETE /admin/users/{id}? The question depends only on the principal’s role or attributes and the operation, which makes it a good fit for a central policy engine. Evaluating it at the gateway or in a sidecar gives you one reviewable policy repository instead of authorization logic scattered across forty services.

Object-level authorization (API1) is different. The decision depends on a specific row: is invoice 8812 owned by the caller’s tenant? A central policy engine can only decide that if it is handed the object’s attributes, which means either fetching them (a coupling and latency cost) or duplicating the data model into policy data. In practice the service, which already loads the object, should perform the ownership check in a shared library or repository layer so that no query can return a row without a scope filter. The gateway can add defence in depth, but it must not be the only line.

Layer 5: detection closes the loop

Preventive controls fail, so every layer emits structured events: gateway access logs with the authenticated subject, policy decisions with the rule that fired, service-level authorization denials, and bot scores. The point is correlation. A single 403 means nothing; two hundred 403s across sequential object IDs from one token is an enumeration campaign. We return to detection engineering after the attack walk-throughs.

Mapping risks to layers

The table below is the working map this architecture implements. “Primary” means the layer that can fully decide the risk; “support” means it reduces exposure but cannot close the gap alone.

OWASP API risk Primary control layer Supporting layers
API1 BOLA Service, ownership scoping in data access Detection of sequential ID access
API2 Broken Authentication Gateway token validation, identity provider Edge credential-stuffing defence, rate limits
API3 Property level authorization Gateway schema, service DTOs Response filtering
API4 Resource consumption Gateway quotas and body limits Edge, service timeouts, cost budgets
API5 BFLA Policy engine at the gateway Service role checks
API6 Sensitive business flows Application logic, bot mitigation Edge bot scoring, flow-level limits
API7 SSRF Service egress controls, URL allow-lists Network egress filtering
API8 Misconfiguration Pipeline policy and config scanning Edge header hardening
API9 Inventory Gateway as the only ingress, spec registry Discovery from traffic logs
API10 Unsafe consumption Service, treat upstream as untrusted Schema validation of responses

Read the table by column rather than by row. If your service layer has no consistent way to scope queries by owner, API1 stays open no matter how much you spend at the edge. If your gateway is not the only path to the backend, API9 stays open, because traffic that bypasses it bypasses every control above.

Workload identity underneath it all

One more foundation sits below the diagram: the services behind the gateway need to authenticate each other, or an attacker who lands inside the network inherits implicit trust. Short-lived workload identities issued per service, as described in SPIFFE and SPIRE workload identity, let the gateway-to-service hop and service-to-service hops carry verifiable identity, so that internal APIs are held to the same authorization standard as public ones. Many real API breaches involve an internal or “private” API that was reachable once an attacker had any foothold.

Attack Walk-Throughs: BOLA, BFLA and Mass Assignment

The three authorization risks share a root cause, which is that the server trusts something the client controls: an identifier, a route, or a field name. Walking through each attack end to end shows exactly which layer could have stopped it and which could not.

BOLA: the request every control approves

Broken Object Level Authorization happens when an endpoint accepts an object identifier from the client and returns or modifies that object without verifying that the caller is entitled to it. The OWASP description calls it the most common and impactful API attack because the identifier can sit in the path, query string, header or body, and every one of those is attacker-controlled.

BOLA attack sequence where an authenticated user substitutes another customer's order ID and every perimeter control approves

Figure 2: A BOLA request sequence. The token, schema, rate and WAF checks all pass; only the ownership check in the service rejects it.

Follow the sequence in Figure 2. The attacker is a legitimate customer with a valid token. They fetch their own order, GET /v1/orders/1043, and notice the numeric identifier. They resend the request as /v1/orders/1044. The edge sees a normal GET. The gateway verifies the token, confirms the path matches /v1/orders/{id} with an integer parameter, and counts one request against a generous quota. The policy engine confirms that role customer may call getOrder. If the service then runs SELECT * FROM orders WHERE id = 1044 and returns the row, the breach is complete, and every control upstream logged it as a success.

Two properties make BOLA durable. First, it scales by scripting: the attacker iterates identifiers at a rate that stays under any sensible limit, harvesting a few thousand records per hour from a single token. Second, switching to random identifiers helps only partially. UUIDs raise the cost of blind guessing, but they leak through logs, shared links, referrer headers and list endpoints, and they do nothing if the check is missing. OWASP’s guidance is explicit that unpredictable IDs are a mitigation, not a substitute for the authorization check.

Here is the vulnerable pattern and the fixed pattern in a typical Python service. The fix is not clever; it is a discipline of never loading an object without the caller’s scope.

# VULNERABLE: loads by id only
@app.get("/v1/orders/{order_id}")
def get_order(order_id: int, user=Depends(current_user)):
    return db.query(Order).filter(Order.id == order_id).one()

# FIXED: ownership is part of the query, so a foreign id is indistinguishable from a missing one
@app.get("/v1/orders/{order_id}")
def get_order(order_id: int, user=Depends(current_user)):
    row = (db.query(Order)
             .filter(Order.id == order_id, Order.tenant_id == user.tenant_id)
             .one_or_none())
    if row is None:
        raise HTTPException(status_code=404)   # same answer for "not yours" and "not found"
    return row

Returning 404 for both “not found” and “not yours” removes an enumeration oracle: with a 403, the attacker learns which identifiers exist. The most robust variant pushes the scope below the application code, for example with PostgreSQL row-level security keyed on a session variable set from the verified token, so a forgotten filter in one handler cannot leak a row.

BFLA: the admin function behind a guessable route

Broken Function Level Authorization is the vertical sibling of BOLA. A regular user discovers that POST /v1/users/{id}/roles or DELETE /v1/admin/exports/{id} accepts their token. The usual causes are frameworks where the route is registered without an authorization decorator, administrative functions mounted under the same prefix as public ones, and HTTP method confusion, for example a GET that is protected while PUT on the same path is not.

Attackers find these by reading client-side JavaScript, mobile app binaries and published OpenAPI documents, then trying verbs and paths against their own token. Unlike BOLA, BFLA is well suited to central policy because the inputs are only principal, operation and sometimes tenant. A deny-by-default rule, “if no policy explicitly allows this operation for this role, reject,” converts a forgotten annotation from a vulnerability into a failed test. We build that policy in the gateway section.

Mass assignment and property-level exposure

API3 covers both directions of the same flaw. On input, mass assignment occurs when a framework binds the entire JSON body onto a model, so a client who adds "is_admin": true or "account_balance": 99999 to a profile update changes fields the endpoint never intended to expose. On output, excessive data exposure occurs when a handler serializes the whole model, including internal flags, hashed credentials or other users’ data, and trusts the client to ignore what it does not need.

The defence is explicit contracts on both sides. Use separate input and output types (data transfer objects) with allow-listed fields, and reject unknown properties rather than silently dropping them, so a probing attacker gets a visible 400 and your logs gain a signal. A schema with additionalProperties: false at the gateway enforces this uniformly, even for services that forgot to. The OpenAPI example later in this post shows it.

API6 and API4: legitimate calls, illegitimate intent

Some of the worst abuse involves no vulnerability. An attacker who automates the checkout endpoint to buy out limited stock, who farms referral credits with synthetic accounts, or who triggers SMS verification to a premium-rate number on repeat is using the API exactly as designed. There is no malformed input to detect and often no single request that looks wrong.

These flows need defences keyed to business meaning: per-account and per-device limits on the sensitive action, step-up verification before high-value steps, cost ceilings on third-party calls triggered by user input, and anomaly detection on funnel behaviour such as time-to-checkout. They are the territory where bot mitigation and application logic meet, and both are covered below. Treat API4 as a financial control, too: any endpoint that triggers a metered downstream cost (messaging, email, model inference, paid lookups) needs a hard budget, not only a requests-per-second cap.

Authentication failures at the API boundary

API2 deserves one paragraph here even though the earlier post covers token mechanics. APIs are attacked through credential stuffing, weak or missing rate limits on login and password reset, long-lived tokens that never rotate, and verification that checks a signature but not the audience or algorithm. The architectural lesson is that authentication endpoints are high-value business flows: they need their own tight limits, their own bot scoring, and monitoring for distributed low-and-slow attempts that a per-IP limit never sees. The same logic applies to API keys, which identify an application rather than a person and should never be treated as proof of user identity.

WAF Rules, Anomaly Scoring and Their Real Limits

A WAF is a pattern-matching filter on HTTP traffic. It is excellent at what pattern matching does well, and structurally unable to do the rest. Understanding the mechanics of the OWASP Core Rule Set (CRS), the most widely deployed open rule set, shows precisely where the boundary lies.

CRS does not block on the first suspicious match by default. Each rule that fires adds to a per-transaction anomaly score according to its severity, and a final evaluation rule compares the accumulated score with a threshold. Per the CRS anomaly scoring documentation, the default scores are 5 for CRITICAL, 4 for ERROR, 3 for WARNING and 2 for NOTICE, and the default thresholds are 5 for requests and 4 for responses. With those defaults, a single critical match blocks an inbound request, while two notices (2 plus 2 equals 4) do not.

WAF anomaly scoring flow from request through CRS rule groups to a blocking decision at the threshold

Figure 3: CRS anomaly scoring. Rules add severity-weighted points; the blocking decision happens once, after all rules have run.

Paranoia levels trade false positives for coverage

CRS organizes rules into four paranoia levels. Level 1 is the default and aims for minimal false positives. Each higher level enables additional, stricter rules that catch more evasions and also flag more legitimate traffic. Level 4 is aimed at very high-security environments and, in practice, demands extensive exclusion tuning. The standard operating pattern is to start at level 1, run in detection-only or with a high threshold, review the logged matches against real traffic, write targeted exclusions, then lower the threshold or raise the level in steps.

APIs complicate this tuning in specific ways. JSON bodies need the JSON body processor enabled so that rules inspect field values rather than the raw blob; many deployments find that free-text fields (comments, descriptions, search queries) legitimately contain SQL keywords or angle brackets and trip injection rules constantly. Mobile and machine clients often lack headers that rules written for browsers expect. Expect to maintain per-route exclusions, ideally generated from the same OpenAPI document that drives your gateway, so that the /search endpoint gets a looser SQLi posture than /login.

What a WAF genuinely catches

The rule groups map to well-known classes: protocol anomalies, scanner user agents, local file inclusion, remote code execution patterns, SQL injection, cross-site scripting, and several framework-specific exploits. For an API, that is still valuable. It blocks commodity scanners, virtual patches a known-vulnerable library while the fix ships, and rejects malformed requests that exploit parser differences. When a critical vulnerability in a popular framework is disclosed, a WAF rule is often the fastest mitigation available, which is its strongest argument.

What it structurally cannot catch

Return to the BOLA request in Figure 2. A WAF sees GET /v1/orders/1044 with a valid bearer token. No rule can distinguish it from /v1/orders/1043 because the difference is semantic: it lives in the relationship between the token’s subject and the row’s owner. The same applies to BFLA, to mass assignment with a plausible field name, to business-flow abuse, and to most authentication logic errors.

There is a second structural limit, which is encoding and context. Signature rules guess how a payload will be interpreted downstream; attackers search for encodings, content types and parser quirks where the WAF and the application disagree. Anomaly scoring raises the bar but does not remove the gap. The accurate framing is that a WAF reduces the volume of generic attacks and buys patching time, while authorization correctness comes from design and tests.

Risk WAF effectiveness Why
SQL injection, XSS, known CVE exploits High Payload patterns are recognisable
SSRF (API7) Low to medium Internal URLs in parameters are legitimate-looking; egress controls matter more
BOLA, BFLA None Requests are well formed and authenticated
Mass assignment Low Needs a schema, not signatures
Resource consumption Medium Rate rules help; cost abuse needs per-identity budgets
Business-flow abuse Low Behaviour, not content

Positive Security: OpenAPI as an Enforcement Contract

The alternative to guessing what is bad is declaring what is good. Most gateways and many WAFs can validate requests against an OpenAPI 3.x description. The specification already contains the data needed: allowed paths and methods, parameter types and ranges, required fields, enumerations and body schemas. Using it as an enforcement artifact means the documentation cannot drift from production behaviour, because production rejects anything the document does not allow.

openapi: 3.0.3
info: {title: Orders API, version: "1.4.0"}
paths:
  /v1/orders/{orderId}:
    get:
      operationId: getOrder
      security: [{oauth2: [orders.read]}]
      parameters:
        - name: orderId
          in: path
          required: true
          schema: {type: integer, minimum: 1, maximum: 2147483647}
      responses:
        "200": {description: ok}
        "404": {description: not found}
  /v1/orders:
    post:
      operationId: createOrder
      security: [{oauth2: [orders.write]}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false      # rejects is_admin, price, owner_id
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 50
                  items:
                    type: object
                    additionalProperties: false
                    required: [sku, qty]
                    properties:
                      sku: {type: string, pattern: "^[A-Z0-9-]{4,32}$"}
                      qty: {type: integer, minimum: 1, maximum: 100}
      responses:
        "201": {description: created}

Three properties of this contract carry the security value. additionalProperties: false turns mass assignment into a hard 400. Bounded integers, array lengths and string patterns shrink the injection and resource-exhaustion surface before any code runs. And the explicit security requirements per operation give the gateway a complete list of which scopes each route needs, so a route with no declared security can be rejected by policy.

Where the schema is not enough

A schema validates shape, not meaning. It will accept orderId: 1044 for any caller. It cannot express cross-field business rules such as “the discount cannot exceed the cart value,” and it cannot verify that a referenced sku exists. Treat schema validation as a cheap, uniform floor, and keep semantic validation in the service. Also validate responses in non-production environments and in contract tests, because a response schema with additionalProperties: false is the most reliable way to catch accidental data exposure before release.

Operationally, version the contract with the code, run schema linting (rules such as requiring a security definition and declared error responses) in CI, and block deployments whose gateway configuration includes routes absent from the spec. That is how the spec stays authoritative rather than decorative.

Gateway Authorization with Open Policy Agent

For function-level authorization, a policy engine gives you deny-by-default semantics, a reviewable policy repository and decisions that are unit-testable. Open Policy Agent (OPA) evaluates policies written in Rego against an input document. Envoy’s external authorization filter, and several gateways, can call OPA per request, passing method, path and verified claims.

package api.authz

import rego.v1

default allow := false

# role -> permitted operations
grants := {
  "customer": {"getOrder", "createOrder", "listMyOrders"},
  "support":  {"getOrder", "listMyOrders", "getCustomer"},
  "admin":    {"getOrder", "createOrder", "listMyOrders", "getCustomer", "deleteExport", "setRole"},
}

allow if {
  some role in input.claims.roles
  input.operation in grants[role]
}

# tenant guard: tenant-scoped operations must carry a tenant claim
deny_reason := "missing tenant" if {
  not input.claims.tenant_id
}

The shape matters more than the syntax. default allow := false means a newly added route is blocked until someone grants it. The operation comes from the matched OpenAPI operationId, not from string-matching a path, which removes a class of bypasses based on path normalization tricks such as double slashes, encoded separators and trailing dots. Test the policy with opa test in CI and keep the decision log, because it doubles as your authorization audit trail.

A practical warning: do not push object-level checks into OPA by fetching the object over the network on every call unless you have measured the latency and designed for failure. If the policy engine is unreachable, you must choose between fail-open (an authorization bypass) and fail-closed (an outage). Choose fail-closed for write paths, keep the engine local as a sidecar, and cache only coarse decisions.

A runnable BOLA test

Policy and schema do not prove object-level correctness; tests do. The most effective control for API1 is an automated two-user test that runs on every build: create resources as user A, then attempt every read, update and delete as user B, and assert that each attempt fails with 403 or 404.

import itertools, requests

BASE = "https://staging.example.internal"
ROUTES = [  # (method, template, creates_with)
    ("GET",    "/v1/orders/{id}",        "/v1/orders"),
    ("PUT",    "/v1/orders/{id}",        "/v1/orders"),
    ("DELETE", "/v1/orders/{id}",        "/v1/orders"),
    ("GET",    "/v1/invoices/{id}",      "/v1/invoices"),
]

def token(user):            # obtain via your test IdP; two users, two different tenants
    return requests.post(f"{BASE}/test/token", json={"user": user}).json()["access_token"]

def hdr(t): return {"Authorization": f"Bearer {t}"}

def test_bola_cross_tenant():
    a, b = token("alice"), token("bob")
    failures = []
    for method, tmpl, create in ROUTES:
        created = requests.post(f"{BASE}{create}", json={"items": [{"sku": "TEST-0001", "qty": 1}]},
                                headers=hdr(a))
        rid = created.json()["id"]
        r = requests.request(method, f"{BASE}{tmpl.format(id=rid)}", headers=hdr(b), json={})
        if r.status_code not in (403, 404):
            failures.append((method, tmpl, r.status_code))
    assert not failures, f"BOLA candidates: {failures}"

Generate the ROUTES list from the OpenAPI document so a new endpoint is covered automatically, and treat any path parameter ending in Id as a candidate. Extend the test to cross-property cases too: user B attempting to set owner_id on their own object. This one test suite, run in CI, closes more real risk than most purchased tooling.

Bot Mitigation and Layered Rate Limiting

Automated abuse is the delivery mechanism for several OWASP entries at once: credential stuffing for API2, enumeration for API1, scraping and scalping for API6, and cost exhaustion for API4. Defending against it means combining identity-aware limits with signals about whether the client is a real, expected program.

Bot mitigation and rate limiting decision flow from edge signals through per-identity limits to allow, challenge or block

Figure 4: Layered bot and rate-limit decisions. Cheap edge signals filter first; per-identity and per-flow limits decide what remains.

Why per-IP limits fail

Credential-stuffing tooling routinely rotates through residential proxy pools, so each source address sends only a handful of attempts. A per-IP limit tuned to stop that traffic would also throttle legitimate users behind carrier-grade NAT or corporate gateways, where thousands of people share an address. The result is the worst of both worlds: bypassed by attackers, hitting customers.

The remedy is to key limits on the dimension the attacker cannot cheaply rotate. For authenticated traffic that is the subject or API key. For login and registration it is the target account (limiting failures per username regardless of source), plus a global rate of failures across all accounts, which spikes visibly during a stuffing campaign. For unauthenticated endpoints it is a composite of device and network signals. Each key supports a different algorithm and budget; the mechanics of token bucket and GCRA implementations, and how to keep counters consistent across gateway replicas, are covered in the distributed rate limiting post.

Four limit scopes that work together

Scope Key Stops Typical response
Edge volumetric Source network, ASN Floods, scanners Drop or challenge
Per consumer Subject or API key Runaway clients, scraping 429 with Retry-After
Per resource and flow Account plus action Enumeration, SMS pumping, referral farming 429, step-up, delay
Global budget Cost of a downstream dependency Cost exhaustion Shed load, degrade

Return HTTP 429 as defined in RFC 6585, include a Retry-After header, and use a problem-details body per RFC 9457 so well-behaved clients can back off. Deliberately differentiate responses for suspected bots: slow them with tarpit delays or return plausible but empty results, rather than immediately signalling the exact threshold that tripped, which teaches the attacker how to tune the campaign.

Bot signals and their reliability

Bot scoring products combine several signal families. Network reputation (known proxy and hosting ranges) is cheap but coarse. TLS and HTTP fingerprints identify the client stack, for example a Python HTTP library masquerading as a mobile app, but can be spoofed by determined tools. Behavioural telemetry from a browser or SDK (timing, input patterns) is stronger but only available where you control the client. For native mobile apps, platform attestation services provide a signal that the request originates from a genuine app build on a genuine device, with the caveat that attestation raises attacker cost without making abuse impossible.

I have not benchmarked any vendor’s detection rates, and published figures are mostly vendor-produced; treat claims of a specific block percentage as unverified until you measure against your own traffic. What generalizes is the economic argument. Bot defence is an arms race of attacker cost versus attacker return, so the goal is to make the abusive flow unprofitable: add friction at the high-value step (a challenge before checkout, not on the home page), bind sessions to devices, and make bulk account creation expensive.

Challenges are a tool, not a wall

CAPTCHA and proof-of-work challenges belong in the “uncertain” band of the decision flow, not at the front door. Solving services and human click-farms defeat plain CAPTCHAs at low cost, and every challenge taxes real users and harms accessibility. Reserve challenges for the middle score range, and for high-risk transitions such as password reset, payout changes and bulk actions. Where a challenge is impractical, which is nearly every machine-to-machine API, rely on identity, quotas and anomaly detection rather than interactive tests.

Inventory, Posture and Detection

You cannot protect an endpoint you do not know exists. API9, Improper Inventory Management, covers forgotten versions (/v1 left running beside /v3), debug and test hosts exposed to the internet, and undocumented endpoints added by one team and never registered. The cheapest structural fix is to make the gateway the only ingress path, enforced at the network layer, and to refuse to route anything not present in the spec registry.

Closing the shadow API gap

Three techniques find what the process missed. Traffic-derived discovery compares observed routes in gateway and load-balancer logs against the registered specs and flags the difference. Cloud-asset scanning enumerates public load balancers, DNS records and serverless function URLs that bypass the gateway. And code scanning finds route definitions in repositories that have no matching spec entry. Review the diff weekly at first; the initial run in most organizations produces a surprising number of orphans, which should be retired, not merely documented. Give each API an owner and a sunset date at registration, and block deployment of a version whose predecessor has outlived its deprecation window.

Detection engineering for authorization abuse

Prevention fails, so design detections around the attack signatures the earlier sections described. The signals worth building first are few and high value:

  • Sequential or high-cardinality object access: one subject touching many distinct object IDs within a window, especially IDs numerically adjacent.
  • Authorization denial ratio: a subject with a high share of 403 and 404 responses on object routes, which signals probing.
  • Function-level probing: a non-admin token calling administrative operations, even if all are denied.
  • Unknown fields: schema rejections for undeclared properties, which indicate mass assignment attempts.
  • Funnel anomalies: impossible time-to-complete for multi-step flows, or many accounts sharing one device fingerprint.

Join these on the authenticated subject, not the IP address, and retain gateway logs long enough to reconstruct a slow campaign. Be careful what you log: bodies and tokens routinely contain personal data and credentials, so log identifiers and decisions, redact secrets, and apply retention rules. Logging is itself an API10 and compliance concern if done carelessly.

Misconfiguration and SSRF

API8 and API7 are controlled in the pipeline and network rather than the request path. Enforce secure defaults with policy-as-code on infrastructure definitions: no wildcard CORS with credentials, no verbose error stacks in production, TLS minimums, no default credentials, security headers set. For SSRF, any feature that fetches a user-supplied URL (webhooks, import-from-URL, avatar fetch) should resolve and validate the destination against an allow-list, block link-local and private ranges including the cloud metadata address, re-validate after redirects, and run the fetcher in a network segment with restricted egress. Moving cloud instances to token-protected metadata services removes the cheapest exploitation path, though it does not replace egress control.

Trade-offs, Gotchas, and What Goes Wrong

Every layer in this architecture has a failure mode that teams discover in production. Knowing them in advance is most of the value of a reference design.

False confidence from a green WAF dashboard. A WAF blocking thousands of requests a day suggests protection, but the blocked traffic is overwhelmingly commodity scanning. Measure authorization coverage, meaning the fraction of object routes with an automated cross-tenant test, and treat that as the primary security metric.

Detection-only that never ends. Teams deploy a WAF in log-only mode to avoid false positives and leave it there for years. Set a calendar deadline for moving to blocking, and track the count of unresolved matches as a burndown.

Schema strictness that breaks clients. additionalProperties: false is correct for requests, but on responses it can break consumers who tolerate extra fields during rolling upgrades. Apply strict request validation in production and strict response validation in tests, and use additive versioning rules.

Policy engine as a single point of failure. A central OPA service that every request depends on becomes an availability risk. Run it as a sidecar or library with locally cached bundles, define fail-closed behaviour deliberately, and alert on stale policy bundles.

Centralised checks that skip internal paths. If service-to-service calls bypass the gateway, an attacker with one compromised service inherits trust everywhere. Apply the same authorization to internal APIs and use workload identity rather than network position.

Rate limits that hurt the wrong people. Aggressive per-IP limits block shared-NAT users and enterprise customers while attackers rotate addresses. Key on identity, publish limits in documentation, and give partners separate quotas.

Logging that becomes the leak. Debug logs holding full request bodies and bearer tokens create a second, less protected copy of sensitive data. Redact at the gateway before logs leave the process.

Over-trusting third-party APIs (API10). Your service calls a partner and passes the response to a template or query without validation. Treat upstream data as untrusted input, validate it against a schema, set timeouts and size caps, and do not follow redirects blindly.

The unifying anti-pattern is control substitution: believing one product category covers a risk that actually needs a different kind of control. WAFs do not replace authorization tests; gateways do not replace ownership checks; bot scores do not replace business-flow limits.

Practical Recommendations

Start with the failures that cause breaches, not the ones that fill dashboards. If you can do only a few things this quarter, do these in order: build the cross-tenant BOLA test suite and gate releases on it; centralise object scoping in the data-access layer, ideally backed by database row-level security; move the gateway to a deny-by-default posture driven by the OpenAPI contract; and key rate limits on identity and flow rather than address.

Then add the edge. Run a managed WAF with CRS at paranoia level 1, in blocking mode with exclusions generated per route, and treat it as virtual patching and noise reduction. Add bot scoring for login, registration, checkout and other flows where automation has a financial payoff, with challenges only in the uncertain score band.

Finally, build the feedback loop: inventory reconciliation, detection rules joined on subject, and a retirement process for old versions. Revisit the architecture whenever you add an exposure type, such as webhooks, GraphQL or AI agents calling your APIs on behalf of users, since each changes the identity and authorization assumptions.

Checklist:

  • [ ] Every object route has an automated cross-tenant test in CI
  • [ ] Data access cannot load an object without a tenant or owner scope
  • [ ] Gateway rejects routes, methods and fields absent from the OpenAPI spec
  • [ ] Policy is deny-by-default, unit-tested and versioned with a decision log
  • [ ] Rate limits exist per consumer, per flow and per downstream budget
  • [ ] WAF is in blocking mode with documented exclusions and a tuning owner
  • [ ] Bot defence covers login, signup and checkout, with a plan for challenges
  • [ ] Gateway is the only ingress, and the spec registry matches live traffic
  • [ ] Detections exist for sequential access, denial ratio and function probing
  • [ ] Every API version has an owner and a sunset date

Frequently Asked Questions

What is the difference between API security and web application security?

Web application security mostly protects pages and forms rendered for browsers, where injection and cross-site scripting dominate. API security protects the data and functions exposed directly to programs, where the typical failures are authorization faults such as BOLA, missing limits, and forgotten endpoints. APIs expose object identifiers and business operations plainly, are called by scripts at machine speed, and often return raw data that the client filters, so logic flaws outweigh payload attacks.

Can a WAF stop BOLA attacks?

No. A BOLA request is syntactically valid, carries a legitimate token and contains no malicious payload; the only wrong thing is that the caller does not own the referenced object. A WAF has no model of ownership. Detection can help, for example by flagging one subject enumerating many IDs, but prevention must come from the service scoping every object query by tenant or owner and from automated cross-user tests.

Does the OWASP API Security Top 10 2023 replace the earlier list?

The 2023 edition supersedes the 2019 list. It merged excessive data exposure and mass assignment into Broken Object Property Level Authorization, broadened rate limiting into Unrestricted Resource Consumption, and added Unrestricted Access to Sensitive Business Flows, Server Side Request Forgery and Unsafe Consumption of APIs. I found no newer API-specific edition at the time of writing, though the separate general OWASP Top 10 was updated in 2025.

How should I tune OWASP CRS for a JSON API?

Begin at paranoia level 1 with the default anomaly thresholds, enable JSON body parsing so rules see field values, and run in detection mode while you review logged matches against real traffic. Write narrow exclusions per route and parameter, such as free-text fields, rather than disabling rules globally. Then enable blocking, and raise paranoia levels only where the false-positive cost is understood. Regenerate exclusions from your OpenAPI file.

Is rate limiting enough to stop credential stuffing?

No. Distributed attackers spread attempts across large proxy pools, so each address stays below any per-IP threshold. Effective defence combines per-account failure limits, a global failure-rate alarm, bot scoring, breached-password checks, multi-factor authentication and step-up challenges for risky logins. Rate limiting is still a necessary layer because it caps damage and cost, but it must be keyed on the targeted account and the identity, not only the source address.

Where should authorization logic live: gateway, policy engine or service?

Split it by what the decision needs. Authentication and coarse scope checks fit the gateway; function-level, role-based rules fit a policy engine such as OPA with deny-by-default semantics; and object-level decisions belong in the service, where the object is already loaded. Pushing object checks into a remote policy engine adds latency and failure modes, so keep the gateway as defence in depth rather than the only barrier.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *