API Security Checklist — OWASP Top 10 for Backend Developers
A practical API security checklist covering authentication, authorization, input validation, rate limiting, and the OWASP API Security Top 10 risks.
You build an API. It works. You deploy it. Then someone finds that changing the user ID in the URL lets them access other people’s data. Or that sending a 10MB JSON body crashes the server. Or that your error messages leak database table names.
API security isn’t a feature you add later — it’s a set of practices you build in from the start. This checklist covers the most common API vulnerabilities and how to prevent them.
The OWASP API Security Top 10
The OWASP API Security Project lists the most critical API security risks. Here’s each one with practical fixes.
1. Broken Object Level Authorization (BOLA)
The #1 API vulnerability. The user changes /api/orders/123 to /api/orders/456 and accesses someone else’s order.
// Vulnerable — no ownership check
@GetMapping("/api/orders/{id}")
fun getOrder(@PathVariable id: String): Order {
return orderRepository.findById(id) // returns any order
}
// Fixed — verify ownership
@GetMapping("/api/orders/{id}")
fun getOrder(@PathVariable id: String, @AuthenticationPrincipal user: User): Order {
val order = orderRepository.findById(id)
?: throw NotFoundException("Order not found")
if (order.userId != user.id && user.role != Role.ADMIN) {
throw ForbiddenException("Access denied")
}
return order
}
Rule: Always verify the authenticated user has permission to access the requested resource. Never rely on the client to send the correct user ID.
2. Broken Authentication
Weak passwords, missing brute-force protection, exposed tokens.
Checklist:
- Use strong password policies (minimum 8 chars, mix of character types)
- Implement account lockout after N failed attempts
- Use bcrypt/scrypt/argon2 for password hashing (never MD5/SHA)
- Set short expiration on access tokens (15 min – 1 hour)
- Rotate refresh tokens on every use
- Invalidate tokens on logout
- Use HTTPS everywhere (no exceptions)
// Use bcrypt for password hashing
val hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt(12))
// Verify
if (!BCrypt.checkpw(inputPassword, storedHash)) {
throw InvalidCredentialsException()
}
3. Broken Object Property Level Authorization
The API returns more data than the client should see:
// Vulnerable — returns everything, including internal fields
@GetMapping("/api/users/{id}")
fun getUser(@PathVariable id: String): User {
return userRepository.findById(id) // includes passwordHash, role, internal notes
}
// Fixed — return a DTO with only public fields
@GetMapping("/api/users/{id}")
fun getUser(@PathVariable id: String): UserResponse {
val user = userRepository.findById(id)
return UserResponse(
id = user.id,
name = user.name,
email = user.email
)
}
Also prevent mass assignment — don’t bind all request fields to the entity:
// Vulnerable — client can set role and isAdmin
@PutMapping("/api/users/{id}")
fun updateUser(@PathVariable id: String, @RequestBody user: User) {
userRepository.save(user) // whatever the client sends
}
// Fixed — explicit field mapping
@PutMapping("/api/users/{id}")
fun updateUser(@PathVariable id: String, @RequestBody request: UpdateUserRequest) {
val user = userRepository.findById(id)
user.name = request.name
user.email = request.email
// role and isAdmin are NOT updatable from this endpoint
userRepository.save(user)
}
4. Unrestricted Resource Consumption
No rate limiting, no request size limits, no pagination limits.
// Vulnerable — no limit on page size
@GetMapping("/api/users")
fun getUsers(@RequestParam pageSize: Int = 100): List<User> {
return userRepository.findAll(PageRequest.of(0, pageSize))
// Client sends pageSize=1000000 → OOM
}
// Fixed — cap the page size
@GetMapping("/api/users")
fun getUsers(@RequestParam pageSize: Int = 20): List<User> {
val cappedSize = pageSize.coerceIn(1, 100)
return userRepository.findAll(PageRequest.of(0, cappedSize))
}
Checklist:
- Set maximum request body size (e.g., 1MB)
- Limit pagination (max 100 items per page)
- Rate limit by IP and by user (e.g., 100 requests/minute)
- Set timeouts on all external calls
- Limit file upload sizes
- Limit query complexity (for GraphQL)
5. Broken Function Level Authorization
Regular users accessing admin endpoints:
// Vulnerable — no role check
@DeleteMapping("/api/users/{id}")
fun deleteUser(@PathVariable id: String) {
userRepository.deleteById(id)
}
// Fixed — require admin role
@DeleteMapping("/api/users/{id}")
@PreAuthorize("hasRole('ADMIN')")
fun deleteUser(@PathVariable id: String) {
userRepository.deleteById(id)
}
Rule: Every endpoint should check both authentication (who are you?) and authorization (are you allowed to do this?).
6. Server-Side Request Forgery (SSRF)
The API makes HTTP requests based on user input:
// Vulnerable — user controls the URL
@PostMapping("/api/fetch")
fun fetchUrl(@RequestBody request: FetchRequest): String {
return httpClient.get(request.url).body() // fetches anything, including internal services
}
An attacker sends url=http://169.254.169.254/latest/meta-data/ and reads your cloud metadata (AWS credentials, etc.).
Fix: Validate and restrict URLs:
- Whitelist allowed domains
- Block internal IP ranges (10.x, 172.16-31.x, 192.168.x, 169.254.x)
- Block localhost and link-local addresses
- Don’t follow redirects blindly
7. Security Misconfiguration
Default configurations that are insecure:
Checklist:
- Disable CORS for
*in production — whitelist specific origins - Remove default error pages that expose stack traces
- Disable HTTP methods you don’t use (TRACE, OPTIONS where not needed)
- Set security headers:
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Strict-Transport-Security - Don’t expose API docs (Swagger/OpenAPI) in production unless intentional
- Remove debug endpoints before deploying
// Don't expose stack traces in production
@ExceptionHandler(Exception::class)
fun handleError(e: Exception): ResponseEntity<ErrorResponse> {
logger.error("Unhandled error", e) // log the full error
return ResponseEntity.status(500).body(
ErrorResponse(message = "Internal server error") // don't send the stack trace
)
}
8. Lack of Protection from Automated Threats
Bots scraping your API, credential stuffing, ticket scalping.
Mitigations:
- Rate limiting per IP and per account
- CAPTCHA for sensitive operations (registration, password reset)
- Device fingerprinting
- Anomaly detection (unusual access patterns)
- Require authentication for expensive operations
9. Improper Asset Management
Old API versions still running, undocumented endpoints, forgotten staging servers.
Checklist:
- Maintain an API inventory (what endpoints exist, who owns them)
- Version your APIs (
/api/v1/,/api/v2/) - Deprecate and remove old versions on a schedule
- Don’t leave staging/test APIs publicly accessible
- Document every endpoint
10. Unsafe Consumption of APIs
Your API calls third-party APIs without validation:
// Vulnerable — trusting third-party response blindly
val response = externalApi.getPrice(productId)
order.price = response.price // what if this is negative or absurdly high?
// Fixed — validate external data
val response = externalApi.getPrice(productId)
require(response.price > 0) { "Invalid price from external API" }
require(response.price < 100_000) { "Price exceeds maximum" }
order.price = response.price
Treat third-party API responses like user input — validate everything.
Quick reference checklist
Authentication
- Passwords hashed with bcrypt/argon2
- Account lockout after failed attempts
- Short-lived access tokens
- Refresh token rotation
- HTTPS only
Authorization
- Every endpoint checks permissions
- Object-level authorization (user owns the resource?)
- Function-level authorization (user has the right role?)
- Admin endpoints protected with role checks
Input validation
- Validate all input (type, length, range, format)
- Use DTOs — don’t bind directly to entities
- Parameterized queries for database access (prevent SQL injection)
- Sanitize output to prevent XSS
Rate limiting & resource protection
- Rate limit per IP and per user
- Maximum request body size
- Pagination limits
- Request timeouts
Error handling
- Don’t expose stack traces
- Don’t expose database details in errors
- Use generic error messages for auth failures
- Log detailed errors server-side
Headers & configuration
- CORS restricted to specific origins
- Security headers set (HSTS, X-Frame-Options, etc.)
- Unused HTTP methods disabled
- Debug endpoints removed in production
Summary
API security comes down to: don’t trust the client, validate everything, and limit what each user can see and do. The OWASP API Top 10 covers the most common attack vectors. Work through the checklist above for every API you build — most vulnerabilities are simple to prevent when you know what to look for.