Web APIs
API Security Best Practices
Learn proven security patterns that protect your APIs from attacks while keeping legitimate users happy.
A single API vulnerability at Equifax exposed 147 million records in 2017. One unpatched endpoint brought down a financial giant. Security is not optional in API design — it is the foundation everything else builds on.Modern APIs handle everything from payment processing to personal data. They sit at the intersection of your internal systems and the wild internet. That intersection is where attackers probe for weaknesses.
But security does not mean making APIs so locked down they become unusable. The best security feels invisible to legitimate users while stopping bad actors cold. This lesson covers the patterns that achieve that balance.
Input Validation and Sanitization
Never trust data coming into your API — not from browsers, not from mobile apps, not from other APIs. Input validation is your first line of defense against injection attacks, data corruption, and system crashes.Attackers send malicious payloads disguised as normal requests. SQL injection attempts hidden in JSON fields. Script tags embedded in user names. Massive payloads designed to overwhelm your parser. Your API needs to catch these before they reach your business logic.
Validation happens at multiple layers. Schema validation checks data structure and types. Business rule validation ensures values make sense in your domain. Sanitization removes or escapes dangerous characters that could cause problems downstream.
// APIForge Security Team: Input validation middleware
const validateUserUpdate = (req, res, next) => {
const { email, age, bio } = req.body;
// Email format validation
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(400).json({
error: 'Invalid email format'
});
}
// Age range validation
if (age && (age < 13 || age > 120)) {
return res.status(400).json({
error: 'Age must be between 13 and 120'
});
}
// Bio length and content validation
if (bio && bio.length > 500) {
return res.status(400).json({
error: 'Bio cannot exceed 500 characters'
});
}
// Remove HTML tags from bio
req.body.bio = bio ? bio.replace(/<[^>]*>/g, '') : bio;
next();
};HTTPS Everywhere
HTTP traffic travels across the internet in plain text — every router, ISP, and coffee shop WiFi can read your API calls. HTTPS encrypts that traffic, making it unreadable to eavesdroppers.TLS encryption protects data in transit between clients and your API servers. API keys, user credentials, personal data, business logic — everything gets scrambled into ciphertext that only the intended recipient can decode.
But HTTPS setup involves more than just buying a certificate. You need proper cipher suite configuration, certificate chain validation, and HTTP Strict Transport Security headers. Modern browsers and API clients expect these security measures.
| Security Feature | What it protects | APIForge implementation |
|---|---|---|
| TLS 1.3 | Data encryption in transit | All API endpoints force TLS 1.3 minimum |
| HSTS Headers | Prevents protocol downgrade attacks | 1-year max-age with includeSubDomains |
| Certificate Pinning | Man-in-the-middle attacks | Mobile apps pin to our certificate authority |
| HTTP Redirect | Accidental plain text requests | Port 80 returns 301 redirect to HTTPS |
Authentication Defense in Depth
Single-factor authentication is not enough for API security — you need multiple verification layers that work together to confirm client identity and prevent unauthorized access.API keys identify applications but not users. OAuth tokens carry user identity but expire. JWT tokens can be self-contained but become stale. The strongest authentication combines multiple factors and verification methods.
Defense in depth means that if one authentication layer fails, others continue protecting your resources. Attackers need to bypass multiple security controls to gain access. This dramatically increases the effort required for successful attacks.
// APIForge Security Team: Multi-layer authentication
const authenticateRequest = async (req, res, next) => {
try {
// Layer 1: API Key validation
const apiKey = req.headers['x-api-key'];
const app = await validateApiKey(apiKey);
if (!app) {
return res.status(401).json({ error: 'Invalid API key' });
}
// Layer 2: JWT token verification
const token = req.headers.authorization?.replace('Bearer ', '');
const payload = await verifyJWT(token);
if (!payload) {
return res.status(401).json({ error: 'Invalid token' });
}
// Layer 3: User account status check
const user = await User.findById(payload.userId);
if (!user || user.status !== 'active') {
return res.status(401).json({ error: 'Account inactive' });
}
// Layer 4: Rate limit per user
const rateLimitKey = `rate_limit:${user.id}`;
const requests = await redis.incr(rateLimitKey);
if (requests === 1) {
await redis.expire(rateLimitKey, 3600); // 1 hour window
}
if (requests > 1000) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
req.app = app;
req.user = user;
next();
} catch (error) {
res.status(500).json({ error: 'Authentication failed' });
}
};Authorization and Permission Controls
Authentication answers "who are you" but authorization determines "what can you do." Proper permission controls ensure users can only access resources they own or have explicit permission to use.Role-based access control assigns permissions to roles, then assigns roles to users. Attribute-based access control makes decisions based on user attributes, resource properties, and environmental conditions. API security often requires both approaches working together.
Permission checks happen at multiple levels. Endpoint-level authorization controls which APIs a user can call. Resource-level authorization determines which specific records they can access. Field-level authorization controls what data they can see within those records.
Error Handling That Protects Information
Error messages walk a fine line — helpful enough for legitimate developers to debug issues, but not so detailed that they reveal system internals to attackers probing for vulnerabilities.Stack traces, database connection strings, file system paths, and internal service names should never appear in API responses. These details help attackers understand your system architecture and find attack vectors.
But generic error messages frustrate developers trying to integrate with your API. The solution is structured error responses that provide useful information without exposing sensitive details. Include error codes, user-friendly messages, and links to documentation.
// APIForge Security Team: Safe error handling
const errorHandler = (error, req, res, next) => {
// Log full error details internally
logger.error('API Error', {
error: error.message,
stack: error.stack,
request_id: req.id,
endpoint: req.path,
user_id: req.user?.id
});
// Determine safe public error response
let publicError = {
error: 'Internal server error',
code: 'INTERNAL_ERROR',
request_id: req.id,
timestamp: new Date().toISOString()
};
if (error.name === 'ValidationError') {
publicError = {
error: 'Request validation failed',
code: 'VALIDATION_ERROR',
details: error.details.map(d => ({
field: d.field,
message: d.message
})),
request_id: req.id
};
} else if (error.name === 'UnauthorizedError') {
publicError = {
error: 'Authentication required',
code: 'UNAUTHORIZED',
request_id: req.id
};
} else if (error.name === 'ForbiddenError') {
publicError = {
error: 'Insufficient permissions',
code: 'FORBIDDEN',
request_id: req.id
};
}
res.status(error.statusCode || 500).json(publicError);
};Request Size Limits and Timeout Protection
Attackers love to send massive payloads that consume server memory and processing time. Request size limits and timeouts prevent these resource exhaustion attacks from bringing down your API infrastructure.JSON parsing libraries can consume enormous amounts of memory when processing deeply nested objects or arrays with millions of elements. A 10MB JSON payload can consume gigabytes of RAM during parsing. Request size limits stop these attacks at the network layer.
Timeout protection prevents long-running requests from tying up server resources. Database queries that should complete in milliseconds sometimes take minutes due to missing indexes or lock contention. Timeouts kill these requests and free up connection pools for legitimate traffic.
Security Headers and CORS Configuration
HTTP security headers tell browsers how to handle your API responses safely. CORS policies control which domains can make JavaScript requests to your endpoints. Both protect against common web-based attacks.Content Security Policy headers prevent script injection attacks by controlling which JavaScript can execute. X-Frame-Options headers stop clickjacking attempts that trick users into making unintended API calls. X-Content-Type-Options prevents MIME type confusion attacks.
CORS determines which web applications can access your API from browsers. Overly permissive CORS policies let malicious websites steal user data by making authenticated requests on their behalf. Restrictive policies break legitimate integrations.
Logging and Monitoring for Security
Security incidents leave traces in your logs if you know what to look for. Proper logging captures authentication attempts, permission failures, and suspicious request patterns that indicate attacks in progress.Log everything security-related but be careful about personally identifiable information. Authentication successes and failures, permission denials, rate limit violations, and validation errors all provide security intelligence. But avoid logging passwords, tokens, or sensitive data.
Real-time monitoring alerts you to attacks as they happen. Multiple failed authentication attempts from the same IP suggest credential stuffing. Unusual traffic patterns might indicate DDoS attacks. Permission violations could mean compromised accounts probing for data they should not access.
Third-Party Security Integration
Your API does not exist in isolation — it integrates with payment processors, cloud services, and external APIs that introduce their own security considerations. Secure integration patterns protect data flowing between systems.API keys for external services need secure storage and regular rotation. Webhook endpoints that receive data from third parties need signature verification to prevent spoofing. OAuth flows with external providers require state parameter validation to prevent CSRF attacks.
Network-level security matters too. API calls to external services should go through egress firewalls that restrict which domains your servers can contact. This prevents data exfiltration if attackers compromise your application code.
The APIForge Security Team needs to integrate with Stripe for payment processing while maintaining their security standards. They implement webhook signature verification, secure API key storage, and network-level restrictions that prevent unauthorized external communications.Security Testing and Vulnerability Assessment
Security is not a one-time implementation — it requires ongoing testing to catch new vulnerabilities as your API evolves. Automated security scanning and manual penetration testing find problems before attackers do.Static analysis tools scan your source code for common security flaws like SQL injection vulnerabilities, hardcoded credentials, and insecure cryptographic implementations. Dynamic analysis tools test your running API by sending malicious payloads and monitoring responses.
Manual security testing finds logic flaws that automated tools miss. Business logic vulnerabilities, race conditions, and complex authorization bypass techniques require human expertise to discover. Regular security audits by experienced professionals provide this deeper analysis.
Quiz
1. The APIForge Security Team discovers attackers are sending SQL injection attempts in JSON request fields. What is the most effective defense strategy?
2. What is the best approach for API security monitoring and incident detection?
3. The APIForge team wants to implement defense-in-depth authentication. Which approach provides the strongest security?