APIs are the backbone of modern software. They connect mobile apps to backends, integrate third-party services, and power the data exchange between microservices. But every API endpoint is also a potential attack surface. If you are building or maintaining APIs, security cannot be an afterthought.
What Makes APIs Vulnerable?
APIs are attractive targets because they expose business logic and data directly. Unlike a web page where interactions are mediated by a browser, API consumers send raw HTTP requests. Attackers can craft any request they want, bypassing any client-side validation you have implemented.
Common API vulnerabilities include:
- Broken authentication: Weak or missing token validation
- Broken authorization: Users accessing data they should not see
- Excessive data exposure: Returning more data than the client needs
- Lack of rate limiting: Enabling brute force and denial of service attacks
- Injection attacks: SQL injection, NoSQL injection, command injection
- Mass assignment: Accepting fields that should not be user-modifiable
The OWASP API Security Top 10 is an excellent reference for understanding the most common and impactful API vulnerabilities.
Authentication Methods
API Keys
API keys are simple tokens included in request headers or query parameters. They identify the calling application but are not tied to a specific user.
Pros: Simple to implement, easy for developers to use. Cons: No user-level permissions, easily leaked in client-side code or logs.
API keys work well for server-to-server communication where you need to identify and rate-limit the calling service. They are not suitable as the sole authentication method for user-facing APIs.
OAuth 2.0
OAuth 2.0 is the industry standard for delegated authorization. It allows users to grant third-party applications limited access to their accounts without sharing credentials.
The most common OAuth 2.0 flow for web applications is the Authorization Code flow:
- User clicks "Login with Provider"
- User authenticates with the provider and grants permission
- Provider redirects back with an authorization code
- Your server exchanges the code for an access token
- Use the access token to make API calls on behalf of the user
OAuth is complex but solves real problems around delegated access. Use an established library rather than implementing it from scratch.
JSON Web Tokens (JWT)
JWTs are self-contained tokens that encode user identity and claims. The server signs the token, and clients include it in subsequent requests. The server verifies the signature without needing to look up a session in a database.
A typical JWT workflow:
POST /api/auth/login/chr(10){chr(10)"email": "user@example.com",chr(10)"password": "secure_password"chr(10)}chr(10)chr(10)Response:chr(10){chr(10)"access": "eyJhbGciOiJIUzI1NiIs...",chr(10)"refresh": "eyJhbGciOiJIUzI1NiIs..."chr(10)}Best practices for JWTs:
- Keep access tokens short-lived (15 to 30 minutes)
- Use refresh tokens to obtain new access tokens
- Store tokens securely (httpOnly cookies, not localStorage)
- Include only necessary claims in the payload
- Use strong signing algorithms (RS256 or ES256 over HS256)
For Django REST Framework, djangorestframework-simplejwt is a solid implementation.
Rate Limiting
Without rate limiting, your API is vulnerable to brute force attacks, credential stuffing, and resource exhaustion. Implement rate limits at multiple levels:
- Global rate limit: Maximum requests per IP per minute across all endpoints
- Endpoint-specific limits: Stricter limits on sensitive endpoints like login and password reset
- User-specific limits: Limits based on the authenticated user or API key
Return proper 429 Too Many Requests responses with a Retry-After header so clients know when to retry. In Django REST Framework, throttling classes handle this:
REST_FRAMEWORK = {chr(10)'DEFAULT_THROTTLE_CLASSES': [chr(10)'rest_framework.throttling.AnonRateThrottle',chr(10)'rest_framework.throttling.UserRateThrottle',chr(10)],chr(10)'DEFAULT_THROTTLE_RATES': {chr(10)'anon': '100/hour',chr(10)'user': '1000/hour',chr(10)}chr(10)}Input Validation
Never trust client input. Validate every field on every request, on the server side. Client-side validation is a convenience for users, not a security measure.
- Type checking: Ensure integers are integers, emails are emails, dates are dates.
- Length limits: Set maximum lengths for all string fields.
- Allowlists over denylists: Define what is allowed rather than trying to block what is dangerous.
- Parameterized queries: Never concatenate user input into database queries. Django's ORM handles this automatically, but be careful with raw SQL.
Serializers in Django REST Framework provide robust validation out of the box. Use them consistently and add custom validation for business rules.
HTTPS Everywhere
Every API request should go over HTTPS. No exceptions. HTTP transmits data including authentication tokens in plain text, making it trivial for anyone on the network to intercept.
With free SSL certificates from Let's Encrypt, there is no excuse for serving an API over HTTP. See our SSL guide for setup instructions.
Additionally, set the Strict-Transport-Security header to tell clients to always use HTTPS.
Logging and Monitoring
You cannot protect what you cannot see. Log every API request with sufficient detail to detect and investigate suspicious activity:
- Timestamp, endpoint, method, response code
- Authenticated user or API key
- IP address and user agent
- Request duration
Do not log sensitive data like passwords, tokens, or personally identifiable information. Use structured logging (JSON format) so logs are easy to search and analyze.
Set up alerts for anomalies: spike in 401 responses, unusual request patterns, requests from unexpected geographic regions, or sudden increases in traffic to specific endpoints.
CORS Configuration
Cross-Origin Resource Sharing (CORS) controls which domains can make requests to your API from a browser. Misconfigured CORS is a common vulnerability.
- *Never use
Access-Control-Allow-Origin:on authenticated endpoints.** This allows any website to make requests to your API using your users' credentials. - Explicitly list allowed origins.
- Limit allowed methods to what your API actually uses.
- Be cautious with
Access-Control-Allow-Credentials: true.
In Django, django-cors-headers provides simple CORS configuration.
Common Mistakes
- Returning too much data: Only return the fields the client needs. Do not expose internal IDs, timestamps, or related data unnecessarily.
- Relying on obscurity: Hidden endpoints are not secure endpoints. Assume attackers will find every route.
- Inconsistent authentication: Every endpoint should require authentication unless it is explicitly public.
- No versioning: API changes can break clients. Version your API from the start (
/api/v1/). - Ignoring error messages: Detailed error messages help attackers. Return generic messages to clients and log details server-side.
API security is not a one-time task. It is an ongoing practice of monitoring, testing, and updating. Start with these fundamentals, and build more advanced protections as your API grows.