API Security: Best Practices and OWASP Top 10
A practical guide to the OWASP API Security Top 10, with implementation patterns, runnable examples, testing guidance, and a production checklist.

Direct answer: Secure an API with layered authentication, object/property/function authorization, input and output validation, rate and abuse limits, SSRF protections, hardened configuration, complete endpoint inventory, safe third-party integration, encryption, and useful audit logs. Use the OWASP API Security Top 10 (2023) as a threat checklist, but design controls around your application’s data and business flows. Authorization deserves special attention: OWASP says, “Authorization remains the biggest challenge in API Security,” and three of the top five risks concern authorization.
What API security covers
API security protects application logic and sensitive data exposed through REST, GraphQL, RPC, webhooks, and internal service APIs. A secure request must pass several independent decisions:
- Identity: Who is calling?
- Authentication: Is the credential valid, unexpired, and intended for this API?
- Authorization: May this principal perform this operation on this object and field?
- Abuse controls: Is the request rate, size, and workflow behavior acceptable?
- Trust boundaries: Are destinations and third-party responses safe to use?
Transport encryption is necessary but insufficient. HTTPS can protect a token in transit while a BOLA bug still lets one authenticated user read another user’s invoice.
OWASP API Security Top 10 (2023)
| Risk | What can go wrong | Primary defenses |
|---|---|---|
| API1: Broken Object Level Authorization | A user changes an ID and accesses another user’s record. | Authorize every object lookup for the requesting principal. |
| API2: Broken Authentication | Weak token validation or credential handling enables account takeover. | Use a trusted identity provider, validate issuer, audience, signature, expiry, and token type. |
| API3: Broken Object Property Level Authorization | Responses expose private fields or updates modify protected properties. | Use explicit response schemas and allowlists for writable fields. |
| API4: Unrestricted Resource Consumption | Large, expensive, or frequent requests exhaust resources. | Apply quotas, throttling, body and pagination limits, and cost monitoring. |
| API5: Broken Function Level Authorization | A normal user invokes an administrative operation. | Check role and privilege for every function, including hidden endpoints. |
| API6: Unrestricted Access to Sensitive Business Flows | Automation abuses signup, checkout, voting, or reservation flows. | Rate limits, workflow state checks, friction, and anomaly detection. |
| API7: Server-Side Request Forgery | A supplied URL makes your server reach internal or unintended hosts. | Allowlist schemes and destinations, resolve DNS safely, and block private ranges. |
| API8: Security Misconfiguration | Debug settings, permissive CORS, default credentials, or inconsistent environments expose data. | Harden defaults, separate environments, and continuously review configuration. |
| API9: Improper Inventory Management | Deprecated versions, debug hosts, or undocumented endpoints remain reachable. | Maintain an owner, version, data classification, and retirement date for every API. |
| API10: Unsafe Consumption of APIs | Untrusted third-party data is treated as safe and triggers injection or logic errors. | Validate, constrain, and monitor every integration response. |
The 2023 list is an awareness framework, not a measured frequency ranking. OWASP reports that it received no public data contributions and used specialist review and community feedback.

Authorization patterns that prevent the most damaging bugs
Check the object on every access
Never rely on an ID being difficult to guess. Load the object through a query constrained by the authenticated principal, or perform an explicit policy check before returning it.
# Python (Flask-style example)
@app.get("/accounts/<account_id>/invoices/<invoice_id>")
def get_invoice(account_id, invoice_id):
user = require_authenticated_user()
invoice = db.query_one(
"SELECT * FROM invoices WHERE id = ? AND account_id = ?",
invoice_id, account_id
)
if invoice is None:
abort(404) # Do not reveal whether another tenant owns it
require_account_member(user, account_id)
return jsonify(public_invoice(invoice))
Apply the same rule to update, delete, export, search, file download, and batch endpoints. Check parent and child relationships, not just the child ID.
Allowlist properties
Build response DTOs from approved fields. For writes, copy only fields the caller may change. Never bind an entire JSON object directly to a privileged model.
// Node.js / Express
const editable = (({ displayName, timezone }) => ({ displayName, timezone }))(req.body);
const account = await Accounts.updateForUser(req.user.id, editable);
res.json({ id: account.id, displayName: account.displayName, timezone: account.timezone });
Separate function permissions
Route-level authentication is not authorization. Require an explicit permission such as invoice:refund for refunds, even if the caller has a valid session.
Authentication and token handling
- Use TLS for every hop and reject plaintext production traffic.
- Validate JWT signature with an allowlisted algorithm, issuer, audience, expiry, and not-before time. Cache keys safely and rotate them.
- Keep access tokens short-lived; use refresh-token rotation and revocation for long sessions.
- Store browser tokens in a design appropriate to your threat model. Avoid exposing long-lived secrets to JavaScript when an HTTP-only, secure, same-site cookie is practical.
- For API keys, show the secret once, hash stored values, scope permissions, record creation and last-use timestamps, and provide revocation.
- Return generic authentication errors and avoid logging credentials, authorization headers, or sensitive payloads.
Input validation, limits, and abuse controls
Validate type, length, encoding, ranges, enum values, and content type at the boundary. Reject unknown fields where possible. Set maximum body size, upload size, array length, query complexity, pagination depth, and execution time. Use per-principal and per-IP limits, then add endpoint-specific budgets for expensive operations.
Rate limiting alone does not secure a business flow. For account creation, checkout, ticket purchase, or password reset, enforce state transitions, one-time tokens, idempotency keys, and velocity checks. Return 429 with a retry hint, and monitor both rejected and successful traffic.
SSRF and outbound request safety
If an endpoint fetches a user-supplied URL, prefer an allowlist of hosts and schemes. Resolve DNS and validate the resulting address before connecting; block loopback, link-local, private, and metadata-service ranges. Disable redirects or revalidate every redirect destination. Use an egress proxy with network policy, short timeouts, response-size limits, and a restricted service identity.
# Python URL validation sketch
from ipaddress import ip_address, ip_network
from urllib.parse import urlparse
PRIVATE = [ip_network("10.0.0.0/8"), ip_network("172.16.0.0/12"),
ip_network("192.168.0.0/16"), ip_network("169.254.0.0/16"),
ip_network("127.0.0.0/8")]
def validate_destination(raw):
parsed = urlparse(raw)
if parsed.scheme not in {"https"} or not parsed.hostname:
raise ValueError("HTTPS URL required")
address = ip_address(resolve_once(parsed.hostname))
if any(address in network for network in PRIVATE):
raise ValueError("private destination blocked")
return parsed
Configuration, inventory, and third-party APIs
Disable debug traces, directory listings, permissive CORS, default passwords, and unnecessary HTTP methods. Keep security settings in version control and scan production configuration for drift. Document every host, route, version, owner, authentication method, data classification, and retirement date. Include shadow and partner APIs in the inventory.
Treat third-party responses as untrusted input. Validate their schema, size, and content; pin or verify signatures where offered; set timeouts and retries with backoff; and prevent a vendor response from selecting arbitrary SQL, templates, commands, or redirect destinations. Record correlation IDs so an incident can be traced across systems.
Practical security workflow
- Model data and actions: list tenants, objects, sensitive fields, roles, and state-changing operations.
- Define policies: write object, property, and function rules before implementing routes.
- Harden the edge: TLS, authentication middleware, schema validation, body limits, CORS, and rate limits.
- Test negative paths: change IDs, remove fields, replay tokens, cross roles, exceed limits, and submit internal URLs.
- Observe: log decision outcomes, principal, object type, route, latency, and correlation ID without secrets.
- Review continuously: update the inventory, rotate keys, retire versions, and investigate anomalies.
Testing checklist
- Can user A read, update, delete, or export user B’s object by changing an identifier?
- Can a client set
isAdmin,ownerId, price, status, or other server-controlled fields? - Do responses expose tokens, password hashes, internal IDs, or fields hidden in the UI?
- Are every administrative route and background job protected by a function-level permission?
- Do oversized bodies, deep pagination, expensive filters, and repeated requests hit safe limits?
- Can redirects, DNS rebinding, IPv6, or alternate numeric IP formats bypass SSRF checks?
- Are old API versions and undocumented hosts discoverable and protected?
- Do dependency and integration failures fail closed without leaking stack traces?
Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Legitimate calls return 401 | Wrong issuer, audience, clock, key, or token scheme. | Inspect decoded claims safely, synchronize clocks, and verify the identity-provider configuration. |
| Users see 403 after login | Authentication exists but the permission or tenant check is missing or incorrect. | Log the policy decision and required permission; test role and object fixtures. |
| Some fields change unexpectedly | Mass assignment or model binding accepts client-controlled properties. | Use explicit writable-field allowlists and server-side ownership assignment. |
| 429 responses spike | Limits are too low, a client retries aggressively, or abuse is underway. | Inspect per-key and per-route metrics, add backoff and idempotency, then tune limits by cost. |
| Outbound fetch reaches internal hosts | Validation checks the original hostname only or follows unsafe redirects. | Resolve and validate each connection destination and enforce egress network policy. |
| Old endpoints keep appearing | Inventory excludes gateway routes, partner hosts, or forgotten versions. | Reconcile gateway logs, DNS, repositories, and deployment manifests; assign owners and retirement dates. |

Performance, reliability, and cost
Security checks add work, but most can be bounded. Cache public key sets and policy metadata with safe expiry. Prefer indexed tenant and object queries. Apply cheap validation before expensive authorization or downstream calls. Put strict timeouts on every network hop, use bounded retries with jitter, and make state-changing operations idempotent. Measure p50 and tail latency for authentication, policy evaluation, database authorization, and rate-limit stores separately.
Budget security infrastructure explicitly: gateway requests, distributed rate-limit storage, audit-log retention, token introspection, and fraud analysis all consume resources. A rejected request that is stopped at the edge costs less than one that reaches a database or payment provider. Keep a fail-closed plan for authorization and a carefully documented degraded mode for non-sensitive reads.
Or skip the browser setup
When your security work includes documenting API behavior or capturing pages for review, ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. Before capture it accepts consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
Minimal request (see the ScreenshotNeo documentation):
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Options cover full-page and selector captures, device presets or custom viewports, dark mode, retina scale, PDF paper and page ranges, custom CSS and JavaScript, clicks, waits, blocked resources, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous webhooks, bulk capture, usage reporting, and an OpenAPI specification. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. There are 1,000 free screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
What is the difference between authentication and authorization?
Authentication establishes who the caller is. Authorization decides what that identity may do with a particular object, field, or function.
Is the OWASP Top 10 a compliance standard?
No. It is an awareness and prioritization framework. Map its categories to your own threat model, controls, and evidence requirements.
Should every endpoint have a different rate limit?
Limits should reflect cost and abuse risk. A cheap metadata read and a report-generation endpoint usually need different budgets.
How should APIs handle unknown JSON fields?
Reject them when compatibility allows; otherwise ignore them safely and ensure they cannot bind to privileged model properties.
When should an API version be retired?
Set a published sunset date, identify consumers from logs, provide a migration path, and block access after the owner confirms usage has ended.


