660 lines
18 KiB
Markdown
660 lines
18 KiB
Markdown
---
|
|
name: api-security-hardening
|
|
description: Secure REST APIs with authentication, rate limiting, CORS, input validation, and security middleware. Use when building or hardening API endpoints against common attacks.
|
|
---
|
|
|
|
# API Security Hardening
|
|
|
|
## Overview
|
|
|
|
Implement comprehensive API security measures including authentication, authorization, rate limiting, input validation, and attack prevention to protect against common vulnerabilities.
|
|
|
|
## When to Use
|
|
|
|
- New API development
|
|
- Security audit remediation
|
|
- Production API hardening
|
|
- Compliance requirements
|
|
- High-traffic API protection
|
|
- Public API exposure
|
|
|
|
## Implementation Examples
|
|
|
|
### 1. **Node.js/Express API Security**
|
|
|
|
```javascript
|
|
// secure-api.js - Comprehensive API security
|
|
const express = require('express');
|
|
const helmet = require('helmet');
|
|
const rateLimit = require('express-rate-limit');
|
|
const mongoSanitize = require('express-mongo-sanitize');
|
|
const xss = require('xss-clean');
|
|
const hpp = require('hpp');
|
|
const cors = require('cors');
|
|
const jwt = require('jsonwebtoken');
|
|
const validator = require('validator');
|
|
|
|
class SecureAPIServer {
|
|
constructor() {
|
|
this.app = express();
|
|
this.setupSecurityMiddleware();
|
|
this.setupRoutes();
|
|
}
|
|
|
|
setupSecurityMiddleware() {
|
|
// 1. Helmet - Set security headers
|
|
this.app.use(helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
scriptSrc: ["'self'"],
|
|
imgSrc: ["'self'", "data:", "https:"]
|
|
}
|
|
},
|
|
hsts: {
|
|
maxAge: 31536000,
|
|
includeSubDomains: true,
|
|
preload: true
|
|
}
|
|
}));
|
|
|
|
// 2. CORS configuration
|
|
const corsOptions = {
|
|
origin: (origin, callback) => {
|
|
const whitelist = [
|
|
'https://example.com',
|
|
'https://app.example.com'
|
|
];
|
|
|
|
if (!origin || whitelist.includes(origin)) {
|
|
callback(null, true);
|
|
} else {
|
|
callback(new Error('Not allowed by CORS'));
|
|
}
|
|
},
|
|
credentials: true,
|
|
optionsSuccessStatus: 200,
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
allowedHeaders: ['Content-Type', 'Authorization']
|
|
};
|
|
|
|
this.app.use(cors(corsOptions));
|
|
|
|
// 3. Rate limiting
|
|
const generalLimiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 100, // limit each IP to 100 requests per windowMs
|
|
message: 'Too many requests from this IP',
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
handler: (req, res) => {
|
|
res.status(429).json({
|
|
error: 'rate_limit_exceeded',
|
|
message: 'Too many requests, please try again later',
|
|
retryAfter: req.rateLimit.resetTime
|
|
});
|
|
}
|
|
});
|
|
|
|
const authLimiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000,
|
|
max: 5, // Stricter limit for auth endpoints
|
|
skipSuccessfulRequests: true
|
|
});
|
|
|
|
this.app.use('/api/', generalLimiter);
|
|
this.app.use('/api/auth/', authLimiter);
|
|
|
|
// 4. Body parsing with size limits
|
|
this.app.use(express.json({ limit: '10kb' }));
|
|
this.app.use(express.urlencoded({ extended: true, limit: '10kb' }));
|
|
|
|
// 5. NoSQL injection prevention
|
|
this.app.use(mongoSanitize());
|
|
|
|
// 6. XSS protection
|
|
this.app.use(xss());
|
|
|
|
// 7. HTTP Parameter Pollution prevention
|
|
this.app.use(hpp());
|
|
|
|
// 8. Request ID for tracking
|
|
this.app.use((req, res, next) => {
|
|
req.id = require('crypto').randomUUID();
|
|
res.setHeader('X-Request-ID', req.id);
|
|
next();
|
|
});
|
|
|
|
// 9. Security logging
|
|
this.app.use(this.securityLogger());
|
|
}
|
|
|
|
securityLogger() {
|
|
return (req, res, next) => {
|
|
const startTime = Date.now();
|
|
|
|
res.on('finish', () => {
|
|
const duration = Date.now() - startTime;
|
|
|
|
const logEntry = {
|
|
timestamp: new Date().toISOString(),
|
|
requestId: req.id,
|
|
method: req.method,
|
|
path: req.path,
|
|
statusCode: res.statusCode,
|
|
duration,
|
|
ip: req.ip,
|
|
userAgent: req.get('user-agent')
|
|
};
|
|
|
|
// Log suspicious activity
|
|
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
console.warn('Security event:', logEntry);
|
|
}
|
|
|
|
if (res.statusCode >= 500) {
|
|
console.error('Server error:', logEntry);
|
|
}
|
|
});
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
// JWT authentication middleware
|
|
authenticateJWT() {
|
|
return (req, res, next) => {
|
|
const authHeader = req.headers.authorization;
|
|
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
return res.status(401).json({
|
|
error: 'unauthorized',
|
|
message: 'Missing or invalid authorization header'
|
|
});
|
|
}
|
|
|
|
const token = authHeader.substring(7);
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
|
|
algorithms: ['HS256'],
|
|
issuer: 'api.example.com',
|
|
audience: 'api.example.com'
|
|
});
|
|
|
|
req.user = decoded;
|
|
next();
|
|
} catch (error) {
|
|
if (error.name === 'TokenExpiredError') {
|
|
return res.status(401).json({
|
|
error: 'token_expired',
|
|
message: 'Token has expired'
|
|
});
|
|
}
|
|
|
|
return res.status(401).json({
|
|
error: 'invalid_token',
|
|
message: 'Invalid token'
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
// Input validation middleware
|
|
validateInput(schema) {
|
|
return (req, res, next) => {
|
|
const errors = [];
|
|
|
|
// Validate request body
|
|
if (schema.body) {
|
|
for (const [field, rules] of Object.entries(schema.body)) {
|
|
const value = req.body[field];
|
|
|
|
if (rules.required && !value) {
|
|
errors.push(`${field} is required`);
|
|
continue;
|
|
}
|
|
|
|
if (value) {
|
|
// Type validation
|
|
if (rules.type === 'email' && !validator.isEmail(value)) {
|
|
errors.push(`${field} must be a valid email`);
|
|
}
|
|
|
|
if (rules.type === 'uuid' && !validator.isUUID(value)) {
|
|
errors.push(`${field} must be a valid UUID`);
|
|
}
|
|
|
|
if (rules.type === 'url' && !validator.isURL(value)) {
|
|
errors.push(`${field} must be a valid URL`);
|
|
}
|
|
|
|
// Length validation
|
|
if (rules.minLength && value.length < rules.minLength) {
|
|
errors.push(`${field} must be at least ${rules.minLength} characters`);
|
|
}
|
|
|
|
if (rules.maxLength && value.length > rules.maxLength) {
|
|
errors.push(`${field} must be at most ${rules.maxLength} characters`);
|
|
}
|
|
|
|
// Pattern validation
|
|
if (rules.pattern && !rules.pattern.test(value)) {
|
|
errors.push(`${field} format is invalid`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
return res.status(400).json({
|
|
error: 'validation_error',
|
|
message: 'Input validation failed',
|
|
details: errors
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
// Authorization middleware
|
|
authorize(...roles) {
|
|
return (req, res, next) => {
|
|
if (!req.user) {
|
|
return res.status(401).json({
|
|
error: 'unauthorized',
|
|
message: 'Authentication required'
|
|
});
|
|
}
|
|
|
|
if (roles.length > 0 && !roles.includes(req.user.role)) {
|
|
return res.status(403).json({
|
|
error: 'forbidden',
|
|
message: 'Insufficient permissions'
|
|
});
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
setupRoutes() {
|
|
// Public endpoint
|
|
this.app.get('/api/health', (req, res) => {
|
|
res.json({ status: 'healthy' });
|
|
});
|
|
|
|
// Protected endpoint with validation
|
|
this.app.post('/api/users',
|
|
this.authenticateJWT(),
|
|
this.authorize('admin'),
|
|
this.validateInput({
|
|
body: {
|
|
email: { required: true, type: 'email' },
|
|
name: { required: true, minLength: 2, maxLength: 100 },
|
|
password: { required: true, minLength: 8 }
|
|
}
|
|
}),
|
|
async (req, res) => {
|
|
try {
|
|
// Sanitized and validated input
|
|
const { email, name, password } = req.body;
|
|
|
|
// Process request
|
|
res.status(201).json({
|
|
message: 'User created successfully',
|
|
userId: '123'
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
error: 'internal_error',
|
|
message: 'An error occurred'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Error handling middleware
|
|
this.app.use((err, req, res, next) => {
|
|
console.error('Unhandled error:', err);
|
|
|
|
res.status(500).json({
|
|
error: 'internal_error',
|
|
message: 'An unexpected error occurred',
|
|
requestId: req.id
|
|
});
|
|
});
|
|
}
|
|
|
|
start(port = 3000) {
|
|
this.app.listen(port, () => {
|
|
console.log(`Secure API server running on port ${port}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
const server = new SecureAPIServer();
|
|
server.start(3000);
|
|
```
|
|
|
|
### 2. **Python FastAPI Security**
|
|
|
|
```python
|
|
# secure_api.py
|
|
from fastapi import FastAPI, HTTPException, Depends, Security, status
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
from slowapi.util import get_remote_address
|
|
from slowapi.errors import RateLimitExceeded
|
|
from pydantic import BaseModel, EmailStr, validator, Field
|
|
import jwt
|
|
from datetime import datetime, timedelta
|
|
import re
|
|
from typing import Optional, List
|
|
import secrets
|
|
|
|
app = FastAPI()
|
|
security = HTTPBearer()
|
|
limiter = Limiter(key_func=get_remote_address)
|
|
|
|
# Rate limiting
|
|
app.state.limiter = limiter
|
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
|
|
# CORS configuration
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[
|
|
"https://example.com",
|
|
"https://app.example.com"
|
|
],
|
|
allow_credentials=True,
|
|
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
|
allow_headers=["Content-Type", "Authorization"],
|
|
max_age=3600
|
|
)
|
|
|
|
# Trusted hosts
|
|
app.add_middleware(
|
|
TrustedHostMiddleware,
|
|
allowed_hosts=["example.com", "*.example.com"]
|
|
)
|
|
|
|
# Security headers middleware
|
|
@app.middleware("http")
|
|
async def add_security_headers(request, call_next):
|
|
response = await call_next(request)
|
|
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
response.headers["X-Frame-Options"] = "DENY"
|
|
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
|
response.headers["Content-Security-Policy"] = "default-src 'self'"
|
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()"
|
|
|
|
return response
|
|
|
|
# Input validation models
|
|
class CreateUserRequest(BaseModel):
|
|
email: EmailStr
|
|
name: str = Field(..., min_length=2, max_length=100)
|
|
password: str = Field(..., min_length=8)
|
|
|
|
@validator('password')
|
|
def validate_password(cls, v):
|
|
if not re.search(r'[A-Z]', v):
|
|
raise ValueError('Password must contain uppercase letter')
|
|
if not re.search(r'[a-z]', v):
|
|
raise ValueError('Password must contain lowercase letter')
|
|
if not re.search(r'\d', v):
|
|
raise ValueError('Password must contain digit')
|
|
if not re.search(r'[!@#$%^&*]', v):
|
|
raise ValueError('Password must contain special character')
|
|
return v
|
|
|
|
@validator('name')
|
|
def validate_name(cls, v):
|
|
# Prevent XSS in name field
|
|
if re.search(r'[<>]', v):
|
|
raise ValueError('Name contains invalid characters')
|
|
return v
|
|
|
|
class APIKeyRequest(BaseModel):
|
|
name: str = Field(..., max_length=100)
|
|
expires_in_days: int = Field(30, ge=1, le=365)
|
|
|
|
# JWT token verification
|
|
def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
|
|
try:
|
|
token = credentials.credentials
|
|
|
|
payload = jwt.decode(
|
|
token,
|
|
"your-secret-key",
|
|
algorithms=["HS256"],
|
|
audience="api.example.com",
|
|
issuer="api.example.com"
|
|
)
|
|
|
|
return payload
|
|
|
|
except jwt.ExpiredSignatureError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Token has expired"
|
|
)
|
|
except jwt.InvalidTokenError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid token"
|
|
)
|
|
|
|
# Role-based authorization
|
|
def require_role(required_roles: List[str]):
|
|
def role_checker(token_payload: dict = Depends(verify_token)):
|
|
user_role = token_payload.get('role')
|
|
|
|
if user_role not in required_roles:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Insufficient permissions"
|
|
)
|
|
|
|
return token_payload
|
|
|
|
return role_checker
|
|
|
|
# API key authentication
|
|
def verify_api_key(api_key: str):
|
|
# Constant-time comparison to prevent timing attacks
|
|
if not secrets.compare_digest(api_key, "expected-api-key"):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid API key"
|
|
)
|
|
return True
|
|
|
|
# Endpoints
|
|
@app.get("/api/health")
|
|
@limiter.limit("100/minute")
|
|
async def health_check():
|
|
return {"status": "healthy"}
|
|
|
|
@app.post("/api/users")
|
|
@limiter.limit("10/minute")
|
|
async def create_user(
|
|
user: CreateUserRequest,
|
|
token_payload: dict = Depends(require_role(["admin"]))
|
|
):
|
|
"""Create new user (admin only)"""
|
|
|
|
# Hash password before storing
|
|
# hashed_password = bcrypt.hashpw(user.password.encode(), bcrypt.gensalt())
|
|
|
|
return {
|
|
"message": "User created successfully",
|
|
"user_id": "123"
|
|
}
|
|
|
|
@app.post("/api/keys")
|
|
@limiter.limit("5/hour")
|
|
async def create_api_key(
|
|
request: APIKeyRequest,
|
|
token_payload: dict = Depends(verify_token)
|
|
):
|
|
"""Generate API key"""
|
|
|
|
# Generate secure random API key
|
|
api_key = secrets.token_urlsafe(32)
|
|
|
|
expires_at = datetime.now() + timedelta(days=request.expires_in_days)
|
|
|
|
return {
|
|
"api_key": api_key,
|
|
"expires_at": expires_at.isoformat(),
|
|
"name": request.name
|
|
}
|
|
|
|
@app.get("/api/protected")
|
|
async def protected_endpoint(token_payload: dict = Depends(verify_token)):
|
|
return {
|
|
"message": "Access granted",
|
|
"user_id": token_payload.get("sub")
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8000, ssl_certfile="cert.pem", ssl_keyfile="key.pem")
|
|
```
|
|
|
|
### 3. **API Gateway Security Configuration**
|
|
|
|
```yaml
|
|
# nginx-api-gateway.conf
|
|
# Nginx API Gateway with security hardening
|
|
|
|
http {
|
|
# Security headers
|
|
add_header X-Frame-Options "DENY" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
add_header Content-Security-Policy "default-src 'self'" always;
|
|
|
|
# Rate limiting zones
|
|
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
|
|
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=1r/s;
|
|
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
|
|
|
|
# Request body size limit
|
|
client_max_body_size 10M;
|
|
client_body_buffer_size 128k;
|
|
|
|
# Timeout settings
|
|
client_body_timeout 12;
|
|
client_header_timeout 12;
|
|
send_timeout 10;
|
|
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name api.example.com;
|
|
|
|
# SSL configuration
|
|
ssl_certificate /etc/ssl/certs/api.example.com.crt;
|
|
ssl_certificate_key /etc/ssl/private/api.example.com.key;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
ssl_prefer_server_ciphers on;
|
|
ssl_session_cache shared:SSL:10m;
|
|
ssl_session_timeout 10m;
|
|
|
|
# API endpoints
|
|
location /api/ {
|
|
# Rate limiting
|
|
limit_req zone=api_limit burst=20 nodelay;
|
|
limit_conn conn_limit 10;
|
|
|
|
# CORS headers
|
|
add_header Access-Control-Allow-Origin "https://app.example.com" always;
|
|
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
|
|
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
|
|
|
|
# Block common exploits
|
|
if ($request_method !~ ^(GET|POST|PUT|DELETE|HEAD)$ ) {
|
|
return 444;
|
|
}
|
|
|
|
# Proxy to backend
|
|
proxy_pass http://backend:3000;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
# Timeouts
|
|
proxy_connect_timeout 60s;
|
|
proxy_send_timeout 60s;
|
|
proxy_read_timeout 60s;
|
|
}
|
|
|
|
# Auth endpoints with stricter limits
|
|
location /api/auth/ {
|
|
limit_req zone=auth_limit burst=5 nodelay;
|
|
|
|
proxy_pass http://backend:3000;
|
|
}
|
|
|
|
# Block access to sensitive files
|
|
location ~ /\. {
|
|
deny all;
|
|
return 404;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### ✅ DO
|
|
- Use HTTPS everywhere
|
|
- Implement rate limiting
|
|
- Validate all inputs
|
|
- Use security headers
|
|
- Log security events
|
|
- Implement CORS properly
|
|
- Use strong authentication
|
|
- Version your APIs
|
|
|
|
### ❌ DON'T
|
|
- Expose stack traces
|
|
- Return detailed errors
|
|
- Trust user input
|
|
- Use HTTP for APIs
|
|
- Skip input validation
|
|
- Ignore rate limiting
|
|
|
|
## Security Checklist
|
|
|
|
- [ ] HTTPS enforced
|
|
- [ ] Authentication required
|
|
- [ ] Authorization implemented
|
|
- [ ] Rate limiting active
|
|
- [ ] Input validation
|
|
- [ ] CORS configured
|
|
- [ ] Security headers set
|
|
- [ ] Error handling secure
|
|
- [ ] Logging enabled
|
|
- [ ] API versioning
|
|
|
|
## Resources
|
|
|
|
- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)
|
|
- [API Security Best Practices](https://github.com/shieldfy/API-Security-Checklist)
|
|
- [JWT Best Practices](https://tools.ietf.org/html/rfc8725)
|