OWASP API Security Top 10 2023: Complete Developer Guide with Real Examples
APIs are the fastest-growing attack surface. The OWASP API Security Top 10 2023 defines the most critical risks. This guide breaks down each risk with real attack examples, vulnerable code patterns, and concrete fixes.
Why APIs Are Now the #1 Attack Surface
Every significant application breach in the last three years has involved an API. Not a buffer overflow in a kernel module or a decade-old CVE — an API endpoint that did exactly what it was told, just by someone who shouldn't have been asking.
APIs have become the primary interface between everything: mobile apps, third-party integrations, microservices, partner systems, internal tools. The average enterprise now exposes hundreds of APIs, many of them undocumented, unmonitored, and tested only for functionality rather than security.
OWASP recognized this shift and published the API Security Top 10 2023 — a complete rewrite of the 2019 edition that reflects how API attacks have evolved. Unlike the web application Top 10, which focuses on server-side vulnerabilities, the API Top 10 addresses issues unique to how APIs are designed, authenticated, and consumed.
This guide walks through all ten risks with the attack pattern, vulnerable code, and the fix.
API1:2023 — Broken Object Level Authorization (BOLA)
What it is: The most common and most impactful API vulnerability. An attacker accesses another user's data by manipulating an object identifier in the request. Attack pattern:GET /api/v1/invoices/1234 ← Attacker's invoice
GET /api/v1/invoices/1235 ← Different user's invoice (just increment the ID)
GET /api/v1/invoices/1236 ← Another user's invoice
Vulnerable code:
// VULNERABLE: Only checks auth, not ownership
app.get('/api/v1/invoices/:id', authenticate, async (req, res) => {
const invoice = await Invoice.findById(req.params.id)
res.json(invoice) // Returns any invoice if you know the ID
})
Fixed code:
// SECURE: Checks ownership before returning data
app.get('/api/v1/invoices/:id', authenticate, async (req, res) => {
const invoice = await Invoice.findOne({
_id: req.params.id,
userId: req.user.id // Must belong to the requesting user
})
if (!invoice) return res.status(404).json({ error: 'Not found' })
res.json(invoice)
})Use non-guessable UUIDs instead of sequential IDs to reduce enumeration risk — but never treat UUIDs as a security control. Authorization logic is the fix.
API2:2023 — Broken Authentication
What it is: Weak authentication mechanisms — missing rate limiting on login endpoints, weak JWT implementation, credentials in query strings, long-lived tokens without rotation. Common vulnerabilities:| Pattern | Problem |
|---|---|
No rate limit on /auth/login | Credential stuffing and brute force |
| JWT signed with HS256 + weak secret | Offline brute-force of the signing key |
JWT with alg: none accepted | Signature verification bypassed |
| Access token valid for 30 days | Long window for stolen token abuse |
| Credentials in URL query params | Logged in proxy/server logs, browser history |
// VULNERABLE: Accepting 'none' algorithm
const decoded = jwt.verify(token, secret, { algorithms: ['HS256', 'none'] })// SECURE: Explicitly restrict to expected algorithm
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] })
Best practices: Short-lived access tokens (15 min) + refresh token rotation, PKCE for OAuth flows, rate limiting with exponential backoff on auth endpoints, MFA for sensitive operations.
API3:2023 — Broken Object Property Level Authorization
What it is: APIs expose more object properties than the caller should be able to see or modify — either over-fetching (exposing sensitive fields) or over-posting (accepting fields that shouldn't be writable). Over-exposure attack:// API returns full user object including internal fields
{
"id": "123",
"email": "user@example.com",
"name": "Alice",
"role": "admin", ← Should not be exposed to end users
"internalUserId": "USR_89",
"creditCardLast4": "4242", ← Sensitive field exposed unnecessarily
"passwordResetToken": "abc" ← Critical: token in response!
}
Mass assignment attack (over-posting):
PATCH /api/v1/profile
{
"name": "Alice",
"email": "alice@example.com",
"role": "admin", ← Attacker adds this, API accepts it
"isVerified": true ← And this
}
Fix: Use explicit allow-lists for response serialization and request deserialization. Never bind request body directly to database models.
// SECURE: Explicit allow-list for what can be updated
const ALLOWED_UPDATE_FIELDS = ['name', 'email', 'bio', 'avatar']app.patch('/api/v1/profile', authenticate, async (req, res) => {
const updates = pick(req.body, ALLOWED_UPDATE_FIELDS) // lodash.pick or equivalent
await User.findByIdAndUpdate(req.user.id, updates)
res.json({ success: true })
})
API4:2023 — Unrestricted Resource Consumption
What it is: No limits on request rate, payload size, query complexity, or execution time — enabling DoS, resource exhaustion, and financial attacks (scraping paid APIs). Attack vectors:- High-frequency requests draining rate-limited paid services you're proxying
- Large file upload payloads consuming memory/disk
- Deep GraphQL queries or nested joins causing CPU spikes
- Webhook abuse — attacker-controlled endpoints causing thousands of outbound calls
// Rate limiting example (express-rate-limit)
const rateLimit = require('express-rate-limit')const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window per IP
message: { error: 'Too many requests' },
standardHeaders: true,
legacyHeaders: false,
})
app.use('/api/', apiLimiter)
// Payload size limits
app.use(express.json({ limit: '10kb' })) // Reject large JSON bodies
app.use(express.urlencoded({ limit: '10kb', extended: true }))
Also: set explicit timeouts on all external calls, implement pagination with max page size, and set GraphQL query depth/complexity limits.
API5:2023 — Broken Function Level Authorization
What it is: Admin or privileged functions are accessible to non-admin users because the API doesn't check the caller's role, only their authentication. Pattern:DELETE /api/v1/admin/users/456 ← Works even as a regular user if no role check
POST /api/v1/internal/export ← "Internal" in the path doesn't mean restricted
Vulnerable pattern:
// VULNERABLE: Only checks authentication
router.delete('/admin/users/:id', authenticate, deleteUser)// SECURE: Checks authentication + role
router.delete('/admin/users/:id', authenticate, requireRole('admin'), deleteUser)
Combine BFLA protection with:
- Separate admin API surface (different subdomain or port, IP-restricted)
- Role claims in JWT that are server-verified on every request
- Audit logging of all privileged function calls
API6:2023 — Unrestricted Access to Sensitive Business Flows
What it is: Legitimate endpoints exploited at scale to abuse business logic — account creation for spam, inventory reservation attacks, coupon/promo code abuse, ticket scalping bots.Unlike other API risks, these endpoints work exactly as designed — just not at the scale or by the actor intended. Examples:
- Bots creating thousands of accounts with fake email addresses
- Competitors scraping your entire product catalog and pricing
- Reserving all available inventory in an e-commerce system then abandoning
- Referral fraud — creating fake accounts to claim referral bonuses
API7:2023 — Server-Side Request Forgery (SSRF)
What it is: User-controlled input causes the server to make HTTP requests to internal services, cloud metadata endpoints, or arbitrary external URLs. Attack in cloud environments:POST /api/v1/fetch-preview
{
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/prod-role"
}The server fetches the AWS metadata endpoint and returns the IAM role credentials to the attacker — potentially giving full AWS account access. Fix:
const { URL } = require('url')
const dns = require('dns').promisesasync function isSafeUrl(inputUrl) {
const parsed = new URL(inputUrl)
// Block non-HTTP protocols
if (!['http:', 'https:'].includes(parsed.protocol)) return false
// Block private/loopback ranges
const addresses = await dns.resolve4(parsed.hostname)
return addresses.every(ip => !isPrivateIP(ip)) // implement isPrivateIP check
}
Also: use an allowlist of permitted external domains instead of a blocklist, deploy in a network where the API server has no access to internal services it doesn't need.
API8:2023 — Security Misconfiguration
What it is: Default configurations, excessive permissions, missing security headers, verbose error messages, unnecessary HTTP methods, and permissive CORS. Most common API misconfigurations:| Misconfiguration | Impact |
|---|---|
CORS: * (allow all origins) | Any website can make credentialed requests to your API |
| Verbose errors in production | Stack traces expose framework versions, file paths, SQL queries |
| HTTP methods not restricted | DELETE, PUT accepted on read-only resources |
| No security headers | Missing X-Content-Type-Options, Strict-Transport-Security |
| Debug endpoints in production | /debug, /actuator, /metrics expose internal state |
| Swagger UI publicly accessible | Exposes full API documentation including internal endpoints |
// CORS: explicit allowlist
app.use(cors({
origin: ['https://yourapp.com', 'https://admin.yourapp.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}))// Generic error responses in production
app.use((err, req, res, next) => {
if (process.env.NODE_ENV === 'production') {
return res.status(500).json({ error: 'Internal server error' }) // No details
}
res.status(500).json({ error: err.message, stack: err.stack })
})
API9:2023 — Improper Inventory Management
What it is: Shadow APIs, deprecated API versions still active, undocumented endpoints, and APIs exposed through third-party services that bypass your security controls. Why this matters: You can't secure what you don't know exists. Shadow APIs often arise from:- API versions never properly deprecated (v1 still active when v3 is current)
- Developer/test APIs promoted to production without security review
- Third-party integrations that create undocumented API exposure
- Microservices that expose internal APIs without going through the API gateway
- API gateway as the single entry point — no direct service-to-internet exposure
- API discovery scanning (Postman API Network, APIsec, Salt Security)
- OpenAPI/Swagger specs enforced as part of CI/CD (PR blocks if spec not updated)
- Regular
nmapscans of your own perimeter to find unexpected open ports/endpoints - Version sunset policy: v1 deprecated → 90 days notice → disabled
API10:2023 — Unsafe Consumption of APIs
What it is: Your application consumes third-party APIs and trusts them unconditionally — not validating responses, following all redirects, or executing received content — making your application an attack vector when the upstream API is compromised. Attack scenario:- Attacker compromises a weather API your application uses
- The compromised API returns a malicious payload in a field your code injects into HTML
- Your users get XSS despite your own code being clean
// Validate and sanitize ALL data from external APIs
const externalData = await thirdPartyApi.getData()// Validate schema
const validatedData = externalApiSchema.parse(externalData) // Zod, Joi, etc.
// Sanitize any string fields before rendering
const safeName = sanitizeHtml(validatedData.name, { allowedTags: [] })
// Never execute content from external APIs
// eval(externalData.script) ← Never do this
Also: use a fixed timeout on all outbound API calls, pin TLS certificates for critical partners, monitor for unexpected response structure changes (could indicate API compromise).
API Security Testing Checklist
Before shipping any API endpoint, verify: Authorization
- [ ] Every endpoint checks object ownership (BOLA), not just authentication
- [ ] Admin/privileged functions check caller's role
- [ ] Object IDs are UUIDs, not sequential integers (defense-in-depth)
- [ ] JWT algorithm explicitly restricted (no
alg: none) - [ ] Token expiry ≤ 15 minutes for access tokens
- [ ] Rate limiting on all auth endpoints
- [ ] Response serialization uses explicit field allow-list
- [ ] Request body allow-list (no mass assignment)
- [ ] Payload size limits configured
- [ ] CORS configured with explicit origin allowlist
- [ ] No verbose error messages in production
- [ ] Swagger/docs not publicly accessible (or restricted to internal network)
- [ ] HTTP methods restricted to what's used
- [ ] Authentication failures logged and alerted
- [ ] Unusual access patterns (BOLA enumeration) detectable in logs
- [ ] API version inventory maintained and documented
The OWASP API Security Top 10 is a framework, not a checklist. The goal isn't to check boxes — it's to build the mental model of how APIs fail. Once you understand that authorization logic must be applied per-object (not just per-endpoint), that JWT implementation bugs are a class of their own, and that your trust in third-party APIs is a liability, the secure patterns become intuitive rather than memorized.
For organizations building APIs at scale, pair this with the DevSecOps CI/CD guide to automate API security testing (DAST, schema validation, auth testing) into your pipeline rather than relying on pre-release manual review.
Microsoft Cloud Solution Architect
Cloud Solution Architect with deep expertise in Microsoft Azure and a strong background in systems and IT infrastructure. Passionate about cloud technologies, security best practices, and helping organizations modernize their infrastructure.
Questions & Answers
Related Articles
Need Help with Your Security?
Our team of security experts can help you implement the strategies discussed in this article.
Contact Us