API Security: Understanding and Preventing the OWASP API Top 10
A comprehensive guide to the most critical API security risks, with real-world examples and practical mitigation strategies.
Why API Security Matters
APIs are the backbone of modern applications. They power mobile apps, microservices, IoT devices, and third-party integrations. But their ubiquity makes them a prime target:
- 83% of web traffic is now API traffic
- APIs are 3x more targeted than traditional web applications
- Average cost of an API breach: $6.1 million
OWASP API Security Top 10 (2023)
API1: Broken Object Level Authorization (BOLA)
The most common and dangerous API vulnerability. Occurs when APIs don’t verify that the user has permission to access a specific object.
Vulnerable Example:
GET /api/v1/users/1234/orders
Authorization: Bearer <token_for_user_5678>
The API returns user 1234’s orders even though the request came from user 5678.
Exploitation:
# Attacker iterates through user IDs
for user_id in range(1, 10000):
response = requests.get(
f"https://api.example.com/users/{user_id}/data",
headers={"Authorization": f"Bearer {stolen_token}"}
)
if response.status_code == 200:
print(f"Leaked data for user {user_id}")
Mitigation:
# Always verify object ownership
def get_user_orders(request, user_id):
# Check if requesting user owns this resource
if request.user.id != user_id and not request.user.is_admin:
raise PermissionDenied("Cannot access other user's orders")
return Order.objects.filter(user_id=user_id)
API2: Broken Authentication
Weak authentication mechanisms that allow attackers to compromise authentication tokens or exploit implementation flaws.
Common Issues:
- No rate limiting on login endpoints
- Weak password requirements
- Sensitive data in JWT payloads
- Long-lived or non-expiring tokens
- Tokens in URLs
Vulnerable JWT:
// Never store sensitive data in JWT payload
{
"user_id": 123,
"email": "user@example.com",
"password_hash": "abc123...", // NEVER DO THIS
"credit_card": "4111..." // NEVER DO THIS
}
Secure Implementation:
# Secure token configuration
JWT_CONFIG = {
"algorithm": "RS256", # Use asymmetric encryption
"access_token_lifetime": timedelta(minutes=15),
"refresh_token_lifetime": timedelta(days=7),
"rotate_refresh_tokens": True,
"blacklist_after_rotation": True,
}
# Rate limiting on auth endpoints
@ratelimit(key='ip', rate='5/m', block=True)
def login(request):
# Authentication logic
pass
API3: Broken Object Property Level Authorization
API exposes object properties that users should not be able to see or modify.
Vulnerable Response:
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"salary": 150000, // User shouldn't see this
"ssn": "123-45-6789", // User shouldn't see this
"is_admin": false // User shouldn't modify this
}
Vulnerable Update:
PATCH /api/users/123
Content-Type: application/json
{
"name": "John Doe",
"is_admin": true // Mass assignment vulnerability
}
Mitigation:
# Define explicit serializers for different contexts
class UserPublicSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'name', 'email'] # Only public fields
class UserPrivateSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'name', 'email', 'salary', 'ssn']
read_only_fields = ['id', 'is_admin'] # Prevent modification
API4: Unrestricted Resource Consumption
APIs that don’t limit how much resource a user can consume, leading to DoS or cost attacks.
Vulnerable Scenarios:
- No pagination limits
- Unlimited file upload sizes
- Complex queries without timeouts
- No rate limiting
Exploitation:
GET /api/users?page_size=1000000
Mitigation:
# Enforce limits
MAX_PAGE_SIZE = 100
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
RATE_LIMIT = "100/hour"
@api_view(['GET'])
@throttle_classes([UserRateThrottle])
def list_users(request):
page_size = min(
int(request.GET.get('page_size', 20)),
MAX_PAGE_SIZE
)
# Paginated response
API5: Broken Function Level Authorization
Users can access administrative functions they shouldn’t have access to.
Vulnerable:
# Regular user can access admin endpoint
GET /api/admin/users
DELETE /api/admin/users/123
Mitigation:
# Decorator-based authorization
from functools import wraps
def admin_required(f):
@wraps(f)
def decorated_function(request, *args, **kwargs):
if not request.user.is_admin:
return JsonResponse(
{"error": "Admin access required"},
status=403
)
return f(request, *args, **kwargs)
return decorated_function
@admin_required
def delete_user(request, user_id):
# Admin-only logic
pass
API6: Unrestricted Access to Sensitive Business Flows
APIs allow automation of business flows that should have human verification or limits.
Examples:
- Automated ticket scalping
- Mass account creation
- Referral bonus abuse
- Coupon/discount abuse
Mitigation:
# Implement business logic controls
class PurchaseView(APIView):
def post(self, request):
user = request.user
# Check purchase velocity
recent_purchases = Purchase.objects.filter(
user=user,
created_at__gte=timezone.now() - timedelta(hours=1)
).count()
if recent_purchases >= 5:
return Response(
{"error": "Purchase limit reached"},
status=429
)
# Check quantity limits
if request.data.get('quantity', 1) > 4:
return Response(
{"error": "Maximum 4 items per purchase"},
status=400
)
# Implement CAPTCHA for suspicious activity
if user.risk_score > 0.7:
if not verify_captcha(request.data.get('captcha')):
return Response(
{"error": "CAPTCHA verification required"},
status=400
)
API7: Server-Side Request Forgery (SSRF)
API fetches remote resources without validating user-supplied URLs.
Vulnerable:
# User can make server fetch any URL
@app.route('/api/fetch-image')
def fetch_image():
url = request.args.get('url')
response = requests.get(url) # SSRF vulnerability
return response.content
Exploitation:
# Access internal metadata service (cloud)
GET /api/fetch-image?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Scan internal network
GET /api/fetch-image?url=http://192.168.1.1:22/
Mitigation:
from urllib.parse import urlparse
import ipaddress
ALLOWED_DOMAINS = ['images.example.com', 'cdn.example.com']
def is_safe_url(url):
parsed = urlparse(url)
# Check scheme
if parsed.scheme not in ['http', 'https']:
return False
# Check against allowlist
if parsed.hostname not in ALLOWED_DOMAINS:
return False
# Check for internal IPs
try:
ip = ipaddress.ip_address(parsed.hostname)
if ip.is_private or ip.is_loopback:
return False
except ValueError:
pass # hostname, not IP
return True
@app.route('/api/fetch-image')
def fetch_image():
url = request.args.get('url')
if not is_safe_url(url):
return "Invalid URL", 400
# Safe to fetch
API8: Security Misconfiguration
Insecure default configurations, incomplete configurations, or misconfigured HTTP headers.
Common Issues:
Misconfigurations:
- Missing security headers (CSP, HSTS, X-Frame-Options)
- CORS allowing all origins (*)
- Verbose error messages exposing stack traces
- Unnecessary HTTP methods enabled
- Default credentials not changed
- Debug mode in production
- Outdated TLS versions
Secure Configuration:
# Django security settings
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'
# CORS configuration
CORS_ALLOWED_ORIGINS = [
"https://app.example.com",
"https://www.example.com",
]
CORS_ALLOW_CREDENTIALS = True
# Disable debug in production
DEBUG = False
API9: Improper Inventory Management
Organizations don’t know all their APIs, leading to unpatched, unmonitored shadow APIs.
Common Issues:
- Old API versions still accessible
- Undocumented endpoints
- Development/staging APIs exposed
- Third-party API integrations forgotten
Solution:
API Inventory Checklist:
Discovery:
- Regular API discovery scans
- Traffic analysis for unknown endpoints
- Code repository scanning
Documentation:
- OpenAPI/Swagger specs for all APIs
- Version deprecation policies
- Clear ownership assignment
Lifecycle:
- Sunset dates for old versions
- Forced migration paths
- Monitoring for deprecated endpoint usage
API10: Unsafe Consumption of APIs
Trusting third-party APIs without proper validation.
Vulnerable:
# Blindly trusting third-party response
def get_user_data():
response = requests.get("https://third-party-api.com/users/123")
data = response.json()
# Directly using in SQL query - SQL injection!
query = f"INSERT INTO users (name) VALUES ('{data['name']}')"
Mitigation:
import bleach
from jsonschema import validate
# Define expected schema
USER_SCHEMA = {
"type": "object",
"properties": {
"name": {"type": "string", "maxLength": 100},
"email": {"type": "string", "format": "email"}
},
"required": ["name", "email"]
}
def get_user_data():
response = requests.get(
"https://third-party-api.com/users/123",
timeout=5 # Always set timeouts
)
data = response.json()
# Validate response structure
validate(instance=data, schema=USER_SCHEMA)
# Sanitize data
sanitized_name = bleach.clean(data['name'])
# Use parameterized queries
cursor.execute(
"INSERT INTO users (name) VALUES (%s)",
[sanitized_name]
)
API Security Checklist
## Pre-Production Checklist
### Authentication & Authorization
- [ ] MFA enabled for sensitive operations
- [ ] JWT using RS256, short expiration
- [ ] Object-level authorization on all endpoints
- [ ] Function-level authorization enforced
### Input Validation
- [ ] All input validated against schema
- [ ] File uploads validated and sandboxed
- [ ] SQL parameterized queries only
- [ ] Output encoding implemented
### Rate Limiting
- [ ] Per-user rate limits
- [ ] Per-IP rate limits
- [ ] Endpoint-specific limits for sensitive operations
- [ ] Pagination limits enforced
### Monitoring
- [ ] All API calls logged
- [ ] Anomaly detection enabled
- [ ] Failed auth attempts alerted
- [ ] Response time monitoring
References
APIs are the front door to your data. Make sure you’re checking who’s knocking.