OWASP API Security Top 10: A Practical Testing Guide
The OWASP API Security Top 10 exists because the web application risk list does not map cleanly onto APIs. APIs have distinct attack surfaces: they expose business logic directly, they speak machine-to-machine, and authorization decisions happen at a much finer-grained level than traditional web apps. The 2023 edition of the list reflects several years of real-world API breach data and tightens the taxonomy considerably from the 2019 version.
This guide works through each of the ten categories in the order OWASP lists them. For each one you will find what the vulnerability actually is, how to probe for it during a test, and what a proper remediation looks like. The HTTP examples use a hypothetical e-commerce API to keep the context consistent throughout.
| ID | Name | 2019 equivalent |
|---|---|---|
| API1:2023 | Broken Object Level Authorization | API1:2019 (unchanged) |
| API2:2023 | Broken Authentication | API2:2019 (refined) |
| API3:2023 | Broken Object Property Level Authorization | Merges API3+API6:2019 |
| API4:2023 | Unrestricted Resource Consumption | API4:2019 (renamed) |
| API5:2023 | Broken Function Level Authorization | API5:2019 (unchanged) |
| API6:2023 | Unrestricted Access to Sensitive Business Flows | New |
| API7:2023 | Server Side Request Forgery | New |
| API8:2023 | Security Misconfiguration | API7:2019 (expanded) |
| API9:2023 | Improper Inventory Management | API9:2019 (unchanged) |
| API10:2023 | Unsafe Consumption of APIs | New |
API1:2023 — Broken Object Level Authorization (BOLA)
BOLA is the most exploited API vulnerability class and has held the top spot since 2019. The root cause is straightforward: an endpoint accepts an object identifier (an order ID, a user ID, a document UUID) as a parameter, and the server does not verify that the authenticated user actually owns or has permission to access that object. Horizontal privilege escalation — accessing another user's data at the same privilege level — is the canonical case.
How to test for BOLA
The testing workflow is mechanical once you have two test accounts at the same privilege level. Authenticate as User A, capture a request that references an object belonging to User A, then replay that request authenticated as User B.
# Capture as User A
GET /api/v1/orders/10482
Authorization: Bearer <token_user_a>
# Replay as User B — expect 403, watch for 200
GET /api/v1/orders/10482
Authorization: Bearer <token_user_b>
If the second request returns the order data, BOLA is confirmed. Do not stop at sequential integer IDs. Test GUIDs (some APIs use UUIDs for IDs but fail to enforce authorization), test indirect references embedded in request bodies, and test IDs that appear as query parameters on GET requests versus path parameters. Also check whether the ID appears in a nested resource: /api/v1/users/99/addresses/14 may enforce the outer user check but skip authorization on the inner address ID entirely.
Vertical BOLA — accessing objects that belong to a higher-privilege tier — deserves its own pass. Authenticate as a regular user and attempt to retrieve objects that logically should be admin-only: audit logs, all-user summaries, internal configuration objects.
Remediation
Authorization checks must happen server-side for every request, not just at login. A common pattern is to scope every data query to the authenticated user's identity: SELECT * FROM orders WHERE id = ? AND user_id = ? rather than SELECT * FROM orders WHERE id = ?. In service-oriented architectures each downstream service must enforce its own authorization — delegating it to the API gateway is insufficient because internal service calls bypass the gateway.
API2:2023 — Broken Authentication
The 2023 revision of this category sharpens the focus onto mechanisms rather than just credential hygiene. It covers weak token generation, poor session management, missing or bypassable multi-factor authentication, and the absence of brute-force protection on authentication endpoints.
How to test for broken authentication
Start with the token itself. JWTs are the dominant API credential format. Check the algorithm field in the header: an alg: none acceptance means the server will accept a completely unsigned token. Check for alg: HS256 on tokens that were originally signed with an RS256 key — some libraries accept both if the verification logic is not explicit about expected algorithm.
# Decode JWT header (base64url)
echo "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0" | base64 -d
# {"alg":"none","typ":"JWT"}
# Craft token with alg:none and arbitrary claims, no signature
# Send as: Bearer <header>.<payload>.
For credential-based authentication, test the login endpoint for rate limiting. Send 50 requests in rapid succession with wrong passwords — if the account is not locked and no CAPTCHA triggers, rate limiting is absent. Check whether the API returns different error messages for "user not found" versus "wrong password" — this enables username enumeration.
Test password reset flows separately. Common weaknesses: reset tokens that are sequential integers, reset tokens that do not expire, reset tokens that are valid for multiple uses, and reset flows that confirm whether an email address exists in the system before sending the email.
Check whether long-lived API keys (service accounts, integrations) can be used indefinitely without rotation, and whether there is any mechanism to detect or revoke a compromised key.
Remediation
Enforce algorithm pinning in JWT verification — reject any token whose header specifies an algorithm other than the one the server was configured to accept. Implement account lockout or progressive rate limiting on all authentication endpoints (login, password reset, token refresh). Ensure token expiry is short for access tokens (15 minutes is a reasonable default) and that refresh tokens are rotated on use. Store credentials as bcrypt, scrypt, or Argon2id hashes — never SHA-1 or MD5.
API3:2023 — Broken Object Property Level Authorization
This category merges what OWASP previously tracked as two separate items: Excessive Data Exposure (the server returns more fields than the client needs) and Mass Assignment (the server blindly binds request body fields to internal model properties). Both stem from the same root failure — the API does not enforce what properties a given client is allowed to read or write.
Excessive data exposure
The pattern is common in APIs built on ORM auto-serialization. The developer returns the entire user object and relies on the frontend to display only a subset of fields. A tester bypasses the frontend entirely.
GET /api/v1/users/me
Authorization: Bearer <token>
# Response includes undocumented fields:
{
"id": 4821,
"email": "user@example.com",
"name": "Alice",
"password_hash": "$2b$12$...",
"internal_role": "admin_pending",
"stripe_customer_id": "cus_abc123",
"mfa_backup_codes": ["48291", "93847"]
}
To test: call every user-facing endpoint directly and examine the raw response for fields that should not be client-visible. Compare the documented API schema (if one exists) against what the server actually returns. Pay particular attention to endpoints that return collections — a list endpoint often leaks more fields than the individual object endpoint.
Mass assignment
The attacker adds fields to a write request that should not be user-controllable. Classic targets are role, is_admin, verified, balance, and plan.
PATCH /api/v1/users/me
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Alice",
"email": "alice@example.com",
"role": "admin",
"is_verified": true
}
If the server updates role or is_verified from this request, mass assignment is confirmed. Test every write endpoint by including extra properties: internal flags, financial fields, audit timestamps, foreign keys to other users' resources.
Remediation
Define explicit allow-lists for both outbound serialization and inbound binding. Do not return the ORM model directly — map it to a response DTO that contains only the fields appropriate for the caller's privilege level. On the write side, use input schemas that explicitly enumerate accepted fields and reject or ignore anything outside that set. In strongly-typed languages, separate your request model from your domain model entirely rather than using the same struct for both.
API4:2023 — Unrestricted Resource Consumption
Previously called "Lack of Resources & Rate Limiting," this category covers any scenario where an API can be made to consume disproportionate resources relative to the cost of the request. This includes traditional rate-limit bypass attacks, but also request-level resource exhaustion: very large payloads, deeply nested structures, expensive database queries triggered by a single API call, and GraphQL query complexity.
How to test
First, test for the presence of rate limiting on high-value endpoints. Authentication endpoints, password reset, account registration, and any endpoint that triggers an external paid action (email send, SMS, payment charge) should have per-IP and per-account rate limits.
# Rapid-fire password reset — 100 requests in 10 seconds
for i in $(seq 1 100); do
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://api.example.com/v1/auth/reset \
-d '{"email":"victim@example.com"}' \
-H "Content-Type: application/json"
done
# All returning 200? Rate limiting absent.
Test for payload size limits. Send a JSON body with a megabyte-scale string value in a field that accepts text. A properly configured API should return 413 Content Too Large. Absent this, the server will parse the entire payload — a useful vector for memory exhaustion.
For GraphQL APIs, test query depth and complexity. A deeply aliased query can fan out to thousands of database calls from a single HTTP request:
query {
user(id: 1) {
friends {
friends {
friends {
friends {
name email orders { id total }
}
}
}
}
}
}
Also test pagination parameters. If the API accepts a limit or page_size parameter, try setting it to 100000. Many APIs fail to enforce a maximum and will attempt to fetch and serialize the requested number of rows.
Remediation
Implement rate limiting at multiple granularities: per-IP (for unauthenticated endpoints), per-account (for authenticated endpoints), and globally per endpoint. Return 429 Too Many Requests with a Retry-After header rather than silently dropping or queuing requests. Set maximum payload sizes in the web server or reverse proxy layer. For GraphQL, implement query depth limits, complexity scoring, and consider query allow-listing in production. Enforce a hard maximum on pagination size — typically 100 to 1000 depending on the object size.
API5:2023 — Broken Function Level Authorization (BFLA)
Where BOLA is about accessing the wrong object, BFLA is about invoking the wrong function — calling an endpoint that your privilege level should not be able to reach. Admin functions are the primary target: user management, configuration changes, data exports, account deletion, and audit log access. The problem typically manifests when developers build separate admin panels that share the same backend API but implement access control inconsistently.
How to test
Enumerate available endpoints through every means available: OpenAPI specs (check /api-docs, /swagger.json, /openapi.json, /v3/api-docs), JavaScript bundles loaded by admin frontends, and path discovery. Once you have a list of admin-scoped endpoints, attempt each one authenticated as a regular user.
# Admin endpoints worth probing as a regular user
GET /api/v1/admin/users
POST /api/v1/admin/users/{id}/promote
DELETE /api/v1/users/{id}
GET /api/v1/admin/audit-logs
POST /api/v1/admin/config
GET /api/v1/internal/metrics
BFLA also appears via HTTP method manipulation. An endpoint might allow GET /api/v1/orders/{id} for all users but also accept DELETE /api/v1/orders/{id} without enforcing that only admins can delete. Test each discovered endpoint with all HTTP verbs: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.
Check for path traversal variants of admin routes: /api/v1/users/me/../admin/users, /api/v1/users/%2F..%2Fadmin%2Fusers. Some routing frameworks normalize paths before matching but after authorization checks.
Remediation
Authorization checks must be tied to the function, not implied by the path prefix. An API gateway rule that blocks /admin/* for non-admin tokens is useful but insufficient — it does not cover the case where admin functionality is exposed at non-admin paths, or where HTTP method authorization differs from path authorization. Use a centralized authorization layer (RBAC or ABAC) that every endpoint invokes explicitly, and conduct access control matrix tests as part of your release pipeline.
API6:2023 — Unrestricted Access to Sensitive Business Flows
New in 2023, this category covers scenarios where the API correctly authenticates and authorizes the caller but does not place any constraints on the volume or pattern of legitimate business operations. The attack is not about breaking authentication — it is about abusing a feature the user is genuinely allowed to use, just not at automated scale.
Classic examples: buying out all inventory of a limited-edition item using scripted bulk purchases, exhausting all available promotional codes via automation, mass-creating accounts to harvest referral bonuses, and scraping price data to undercut competitors in real time.
How to test
Identify endpoints that trigger economically significant actions: order creation, voucher redemption, referral claim, review submission, account creation. Verify whether these endpoints enforce any per-account velocity limits, require proof of human interaction (CAPTCHA, device fingerprint), or detect anomalous usage patterns.
# Attempting to redeem the same promo code 50 times across 50 accounts
POST /api/v1/cart/apply-promo
Content-Type: application/json
{"code": "LAUNCH50"}
# Does the 51st account get a different response? Are accounts flagged?
# Can the same device/IP create unlimited accounts?
Test the full purchase flow end to end: add to cart, apply discount, checkout. Check whether reserve-and-release logic exists — does adding an item to cart reduce available inventory? If so, can you lock out legitimate buyers by holding items in carts without purchasing?
Remediation
Implement per-account velocity limits on sensitive business operations, distinct from the general rate limiting that addresses API4. Layer in device fingerprinting and behavioral anomaly detection for high-value flows. Consider requiring CAPTCHA or step-up authentication (MFA re-prompt) before economically significant actions. For inventory-sensitive operations, use soft-hold timeouts so cart reservations expire if not completed. Log and alert on unusual per-account action rates.
API7:2023 — Server Side Request Forgery (SSRF)
Also new in 2023, SSRF via API parameters is a distinct enough pattern from traditional SSRF to warrant its own slot in the API list. The attack surface is any API endpoint that accepts a URL — or a hostname, IP address, or file path — as a user-controlled parameter and then causes the server to make a network request to that target.
Common entry points: webhook registration (the user provides a callback URL), avatar import by URL (POST /users/avatar { "url": "https://example.com/photo.jpg" }), import/export features that fetch remote files, PDF generation services that render user-supplied URLs, and health-check or preview endpoints.
How to test
The primary test target in cloud environments is the instance metadata service. In AWS it responds at 169.254.169.254; in GCP and Azure at the same address but with a different API. If you can make the server request that address, you can retrieve IAM credentials.
# Classic SSRF via webhook URL parameter
POST /api/v1/webhooks
Content-Type: application/json
{
"event": "order.created",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
}
# IMDSv1 AWS — no additional headers required
# Response body will contain role name if vulnerable
# Test for internal service access
{
"url": "http://internal-db.company.local:5432/"
}
# DNS rebinding / protocol smuggling
{
"url": "http://attacker-controlled-domain.com/"
# DNS resolves to 10.0.0.1 on second lookup
}
Use a collaborator tool (Burp Collaborator, interactsh) to catch out-of-band DNS lookups even when the server does not return the fetched content in the response — blind SSRF is common.
Test for protocol support beyond HTTP. Some request libraries will follow file://, dict://, gopher://, or ftp:// URLs if not explicitly restricted.
Remediation
Validate all user-supplied URLs against an allow-list of approved domains before making the request. Resolve the hostname to an IP address and verify the IP is not in a private, loopback, link-local, or multicast range — this check must happen after DNS resolution to prevent DNS rebinding. Enforce IMDSv2 on AWS instances (requires a PUT request with a session token header, which HTTP clients following a simple redirect will not do). Disable unused URL schemes in your HTTP client. Run the outbound request from a network-isolated component with no access to internal services wherever possible.
API8:2023 — Security Misconfiguration
This is the broadest category in the list. It covers every case where the API is deployed with a configuration that increases the attack surface beyond what the intended functionality requires. The most impactful misconfigurations in practice: permissive CORS policies, verbose error messages that leak stack traces and internal paths, unnecessary HTTP methods enabled on all endpoints, default credentials left on API management platforms, and missing TLS or weak cipher suites.
How to test
Start with a simple OPTIONS request to any endpoint — the response headers reveal which methods are accepted:
OPTIONS /api/v1/users HTTP/1.1
Host: api.example.com
# Response
Allow: GET, POST, PUT, DELETE, PATCH, TRACE, OPTIONS
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: *
A wildcard Access-Control-Allow-Origin: * combined with credentials is an immediate finding. Check whether the CORS policy reflects the Origin header back unvalidated — a common misconfiguration that allows any domain to make credentialed cross-origin requests.
# Trigger a verbose error
GET /api/v1/orders/not-a-valid-id
# Look for stack traces, file paths, ORM query details in the response body
Test for TRACE method support — it can be used to read HTTP-only cookies in some configurations. Check whether the API is accessible over plain HTTP and whether the redirect to HTTPS (if present) leaks the Authorization header. Review response headers for security posture: X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, and Content-Security-Policy should all be present.
For API management platforms (Kong, Apigee, AWS API Gateway, MuleSoft), check whether the admin interface is exposed to the internet and whether default credentials are in use.
Remediation
Define CORS policy explicitly per endpoint based on the expected calling origins — never use * for authenticated APIs. Return generic error messages to clients and log the full detail server-side only. Disable HTTP methods not required by the API at the web server or gateway layer. Enforce HTTPS everywhere and use HSTS with a long max-age. Disable TRACE. Apply security headers consistently. Automate security configuration checks as part of your deployment pipeline so regressions are caught before reaching production.
API9:2023 — Improper Inventory Management
APIs accumulate technical debt fast. Versioning strategies leave old endpoints active. Internal services get exposed unintentionally. Documentation falls behind the implementation. The result is a shadow API surface — endpoints that exist in production but are not monitored, not hardened, and often not even known to the security team.
The risk is compounded because old API versions typically pre-date whatever security controls were added to the current version. An endpoint in /api/v1/ that was deprecated when /api/v2/ launched may still accept unauthenticated requests if the authentication middleware was added to v2 only.
How to test
Version enumeration is straightforward. If the target uses /v2/, probe /v1/, /v0/, /beta/, /legacy/, /internal/, /api/ (without version), and date-based paths like /2023-01/.
# Version probe
curl -I https://api.example.com/v1/users
curl -I https://api.example.com/v0/users
curl -I https://api.example.com/beta/users
curl -I https://api.example.com/internal/users
curl -I https://api.example.com/api/users
Beyond versioning, look for undocumented endpoints by mining JavaScript bundles from the frontend application, examining mobile app APKs (decompile with jadx or apktool), reviewing Wayback Machine snapshots of API documentation, and checking Postman collections published in the API's GitHub repository or documentation site.
Check whether the staging and development environments are accessible from the internet. These environments often have weaker authentication, debug endpoints enabled, and real production data mirrored for testing.
Remediation
Maintain an authoritative API inventory that is tied to the deployment pipeline — every deployed endpoint must be registered. Establish a sunset policy for deprecated API versions: announce end-of-life dates, monitor usage, and actually decommission endpoints when the date arrives. Apply the same security controls (authentication, rate limiting, logging) to every API version, not just the current one. Restrict access to non-production environments by IP allow-list or VPN — they must not be reachable from the public internet.
API10:2023 — Unsafe Consumption of APIs
The final category inverts the perspective: instead of an attacker targeting your API, the risk is in your API consuming third-party APIs without adequate validation of what comes back. Modern APIs are integrations, not monoliths. They pull data from payment processors, identity providers, enrichment services, mapping APIs, and dozens of other external sources. If that data flows from the third-party response into your application without sanitization, the third party becomes a vector for injection into your system.
This is not hypothetical. Supply-chain attacks against SaaS APIs have escalated substantially. A compromised enrichment service can push SQL injection payloads, XSS, or path traversal strings into your application if you treat third-party data as trusted.
How to test
Map the data flows: what third-party APIs does your application consume, what fields do those APIs return, and where do those fields end up in your application — in database queries, in HTML responses, in file paths, in shell commands?
# Example: your API calls a geocoding service and stores the returned address
GET https://geocoding-api.example.com/v1/lookup?ip=1.2.3.4
# Response
{
"city": "Amsterdam",
"country": "Netherlands",
"isp": "Acme <script>alert(1)</script> Corp"
}
# If "isp" is stored and later rendered in an admin dashboard without escaping, stored XSS.
Test by intercepting the response from the third-party API (using a proxy or by routing through a controlled mock service) and injecting attack strings into every field your application consumes: SQL metacharacters, HTML/JS payloads, null bytes, path traversal sequences, and CRLF injection strings.
Also validate that your application handles third-party API failures gracefully. What happens when the external service returns a 500, an empty body, a malformed JSON response, or an unexpectedly large payload? Error handling that leaks internal state is itself a finding.
Remediation
Apply the same input validation to third-party API responses that you apply to user-supplied data. Schema-validate third-party responses against a defined contract (JSON Schema or similar) before processing them. Sanitize string fields before interpolating them into queries, templates, or file paths — do not assume that an external service you do not control will never return malicious content. Apply size and character-set constraints on fields even when the source is trusted. Monitor third-party API behavior for anomalies: a sudden change in response structure may indicate a compromise upstream.
Putting It Together: An API Security Test Checklist
A structured API security test against the full OWASP API Top 10 typically follows this sequence:
- Inventory first. Collect all API specs, enumerate versions, check for shadow endpoints before writing a single exploit payload.
- Authentication and token analysis. Decode and inspect every token format. Test login and password-reset endpoints for rate limiting and enumeration.
- Authorization matrix. Build a table of all endpoints versus all privilege levels. Test each cell: can a lower-privilege role reach a higher-privilege function or object?
- Data exposure review. Call every endpoint and diff the response against the documented schema. Look for fields that should not be client-visible. Test write endpoints with extra properties.
- Resource consumption tests. Rate-limit probes, oversized payloads, deep pagination, GraphQL complexity.
- Business flow abuse. Identify economically significant operations and test velocity controls.
- SSRF probes. Identify all URL-accepting parameters and test for internal access and cloud metadata.
- Configuration review. CORS, error verbosity, HTTP methods, security headers, TLS.
- Third-party consumption. Map downstream API calls and test response handling with injected payloads.
Automated scanning handles the mechanical parts of this workflow — enumeration, fuzzing, header analysis, rate-limit probes — but authorization testing requires two distinct credential sets and an understanding of the application's intended access model. The best results come from combining automated coverage with targeted manual verification of the authorization matrix.
Ironimo automates API security testing against the OWASP API Top 10 — scanning REST and GraphQL APIs for BOLA, authentication weaknesses, excessive data exposure, and injection vulnerabilities without a manual setup process.