API Security Landscape
APIs (Application Programming Interfaces) have become the primary attack surface for modern applications. As businesses adopt microservices, mobile apps, and third-party integrations, APIs handle more sensitive data and business logic than traditional web interfaces. Gartner predicts that by 2025, API attacks will be the most frequent attack vector causing data breaches for enterprise web applications.
API security differs from traditional web security in critical ways:
- Direct data exposure: APIs return structured data (JSON, XML) without UI-imposed constraints, allowing attackers to request more data than intended
- Machine-to-machine: No human in the loop means attacks can scale to millions of requests per second
- Third-party access: APIs are exposed to partners, mobile apps, and JavaScript clients that developers do not fully control
- Stateless nature: REST APIs are typically stateless, making traditional session-based security controls less applicable
High-profile API breaches demonstrate the impact: Peloton (2021) exposed 3 million user accounts via API authentication bypass, T-Mobile (2021) suffered a breach affecting 76 million customers through API exploitation, and the Capital One breach (2019) exposed 100 million records via SSRF in a cloud API.
OWASP API Security Top 10
The OWASP API Security Top 10 (2023) identifies the most critical API-specific vulnerabilities. While related to the main OWASP Top 10, this list focuses on vulnerabilities unique to API design and implementation.
API1:2023 - Broken Object Level Authorization
APIs expose endpoints that handle object identifiers (user IDs, resource IDs). Without proper authorization checks, attackers manipulate IDs to access unauthorized resources.
Vulnerable (Node.js/Express):
// VULNERABLE: No authorization check
app.get('/api/users/:userId/profile', (req, res) => {
const profile = db.users.find(req.params.userId);
res.json(profile); // Returns ANY user's profile!
});
Secure (Node.js/Express):
// SECURE: Verify ownership before returning data
app.get('/api/users/:userId/profile', authenticateToken, (req, res) => {
if (req.params.userId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
const profile = db.users.find(req.params.userId);
res.json(profile);
});
API2:2023 - Broken Authentication
Weak authentication mechanisms, exposed credentials, missing rate limiting on authentication endpoints.
Vulnerable (Python/Flask):
# VULNERABLE: API key in URL, no rate limiting
@app.route('/api/data')
def get_data():
api_key = request.args.get('api_key') # Exposed in logs!
if api_key == 'hardcoded-key': # Weak check
return jsonify(data)
return 'Unauthorized', 401
Secure (Python/Flask):
# SECURE: API key in header, hashed comparison, rate limiting
from flask_limiter import Limiter
from werkzeug.security import check_password_hash
limiter = Limiter(app, key_func=lambda: request.headers.get('X-API-Key'))
@app.route('/api/data')
@limiter.limit("100 per hour")
def get_data():
api_key = request.headers.get('X-API-Key')
if not api_key:
return jsonify({'error': 'Missing API key'}), 401
stored_hash = db.api_keys.get_hash(api_key[:8]) # Key prefix lookup
if not stored_hash or not check_password_hash(stored_hash, api_key):
return jsonify({'error': 'Invalid API key'}), 401
return jsonify(data)
API3:2023 - Broken Object Property Level Authorization
APIs return more fields than the user should access, or allow updating fields that should be read-only (mass assignment).
Vulnerable (Java/Spring):
// VULNERABLE: Returns sensitive fields
@GetMapping("/api/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).get(); // Returns SSN, password hash, etc!
}
Secure (Java/Spring):
// SECURE: DTO pattern to control exposed fields
@GetMapping("/api/users/{id}")
public UserPublicDTO getUser(@PathVariable Long id, Authentication auth) {
User user = userRepository.findById(id).orElseThrow();
// Authorization check
if (!user.getId().equals(auth.getUserId()) && !auth.isAdmin()) {
throw new ForbiddenException();
}
// Return only public fields
return new UserPublicDTO(user.getId(), user.getName(), user.getEmail());
}
API4:2023 - Unrestricted Resource Consumption
Missing rate limiting, pagination limits, or max file upload sizes enable resource exhaustion attacks.
Vulnerable (Go):
// VULNERABLE: No pagination limit
func GetUsers(w http.ResponseWriter, r *http.Request) {
users := db.GetAllUsers() // Could return millions of records!
json.NewEncoder(w).Encode(users)
}
Secure (Go):
// SECURE: Pagination with max limit and rate limiting
import "github.com/ulule/limiter/v3"
func GetUsers(w http.ResponseWriter, r *http.Request) {
// Rate limiting
context := limiter.Get(r.Context(), "global")
if context.Reached {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
// Pagination with max limit
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 || limit > 100 {
limit = 20 // Default to 20, max 100
}
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
users := db.GetUsers(limit, offset)
json.NewEncoder(w).Encode(users)
}
API5:2023 - Broken Function Level Authorization
Administrative functions accessible to regular users due to missing authorization checks.
Vulnerable (TypeScript/Express):
// VULNERABLE: Admin route with no role check
app.delete('/api/users/:id', authenticateToken, (req, res) => {
db.users.delete(req.params.id); // Any authenticated user can delete!
res.json({ success: true });
});
Secure (TypeScript/Express):
// SECURE: Role-based access control middleware
function requireAdmin(req: Request, res: Response, next: NextFunction) {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
app.delete('/api/users/:id', authenticateToken, requireAdmin, (req, res) => {
db.users.delete(req.params.id);
res.json({ success: true });
});Rate Limiting and Throttling
Rate limiting prevents API abuse, brute-force attacks, and resource exhaustion. Implement multiple rate limiting strategies:
Token Bucket Algorithm
Allows bursts up to bucket capacity, refilling tokens at a steady rate.
Implementation (Node.js/Express):
import rateLimit from 'express-rate-limit';
// Global rate limit: 100 requests per 15 minutes
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/', globalLimiter);
// Strict limit for authentication: 5 attempts per hour
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true // Only count failed attempts
});
app.post('/api/login', authLimiter, loginHandler);
Sliding Window Rate Limiting
Implementation (Python/Flask):
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"],
storage_uri="redis://localhost:6379"
)
@app.route('/api/data')
@limiter.limit("10 per minute")
def api_data():
return jsonify(data)
# Different limits for different tiers
@app.route('/api/premium')
@limiter.limit("100 per minute", key_func=lambda: request.headers.get('X-API-Key'))
def premium_api():
return jsonify(premium_data)
Rate Limiting Headers
Return rate limit information in response headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1640000000Input Validation and Sanitization
API input validation prevents injection attacks, data corruption, and logic errors. Validate all inputs at the API boundary.
Schema Validation
Vulnerable (No Validation - Node.js):
// VULNERABLE: No input validation
app.post('/api/users', (req, res) => {
const user = db.users.create(req.body); // Accepts ANY fields!
res.json(user);
});
Secure (Schema Validation - Node.js/Zod):
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(18).max(120)
});
app.post('/api/users', (req, res) => {
try {
const validated = userSchema.parse(req.body);
const user = db.users.create(validated);
res.json(user);
} catch (err) {
res.status(400).json({ error: err.errors });
}
});
Secure (Pydantic Validation - Python/FastAPI):
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, conint, constr
class UserCreate(BaseModel):
email: EmailStr
name: constr(min_length=1, max_length=100)
age: conint(ge=18, le=120)
@app.post("/api/users")
def create_user(user: UserCreate):
# Pydantic automatically validates
db_user = db.users.create(user.dict())
return db_user
Secure (Bean Validation - Java/Spring):
import javax.validation.Valid;
import javax.validation.constraints.*;
public class UserCreateDTO {
@Email
@NotNull
private String email;
@NotBlank
@Size(min = 1, max = 100)
private String name;
@Min(18)
@Max(120)
private Integer age;
}
@PostMapping("/api/users")
public User createUser(@Valid @RequestBody UserCreateDTO dto) {
return userRepository.save(new User(dto));
}GraphQL-Specific Security Concerns
GraphQL introduces unique security challenges beyond REST APIs.
Query Depth Limiting
GraphQL allows nested queries that can cause exponential database load.
Vulnerable (No Depth Limit):
// Attacker query:
query {
user(id: 1) {
posts {
comments {
author {
posts {
comments {
author { ... } // Infinite nesting!
}
}
}
}
}
}
}
Secure (Depth Limiting - Node.js/Apollo):
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)] // Max depth: 5 levels
});
Query Complexity Analysis
Implementation (Node.js/GraphQL):
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
validationRules: [
createComplexityLimitRule(1000, {
onCost: (cost) => console.log('query cost:', cost),
formatErrorMessage: (cost) => `Query too complex: ${cost}. Maximum allowed: 1000`
})
]
});
Introspection in Production
Disable GraphQL introspection in production to prevent schema enumeration:
const server = new ApolloServer({
introspection: process.env.NODE_ENV !== 'production'
});How CodeSlick Scans API Code
CodeSlick provides specialized API security analysis across JavaScript, TypeScript, Python, Java, and Go:
- Authentication gaps: Detects API endpoints missing authentication middleware, hardcoded API keys, and weak JWT validation
- Authorization issues: Identifies missing ownership checks (IDOR), missing role-based access control, and privilege escalation vectors
- CORS misconfigurations: Flags overly permissive CORS policies (
Access-Control-Allow-Origin: *) with credentials enabled - Input validation: Detects missing input validation, SQL injection, NoSQL injection, and command injection patterns
- Rate limiting gaps: Identifies authentication and sensitive endpoints without rate limiting
- API key exposure: Detects API keys in URLs, logs, and client-side code (38 secret patterns)
All findings include CWE classification, CVSS scoring, OWASP API Top 10 mapping, and framework-specific remediation code for Express.js, Flask, FastAPI, Spring Boot, and Go.
CodeSlick runs on every pull request via the GitHub App, on every commit via the CLI, and on-demand at codeslick.dev/analyze.
Scan your API code for authentication gaps CORS misconfigurations and input validation issues across 5 languages.