Why API Security Matters
APIs are the backbone of modern applications, handling everything from user authentication to payment processing. A single security flaw in your API can expose customer data, enable unauthorized access, or even take down your entire service.
According to Gartner, API attacks are now the most frequent attack vector for enterprise web applications. Startups are particularly vulnerable because they often prioritize speed over security in early stages.
⚠️ Reality Check: 83% of organizations experienced an API security incident in the past year, with the average cost of a data breach reaching $4.5M. Don't become a statistic.
1. Authentication & Authorization
✓ Use Strong Authentication
Never roll your own authentication. Use established standards like OAuth 2.0 or JWT tokens with proper validation.
import jwt
from datetime import datetime, timedelta
from functools import wraps
SECRET_KEY = os.getenv('JWT_SECRET_KEY') # Never hardcode!
ALGORITHM = 'HS256'
TOKEN_EXPIRY = timedelta(hours=1)
def create_access_token(user_id: str) -> str:
"""Create a secure JWT token"""
payload = {
'user_id': user_id,
'exp': datetime.utcnow() + TOKEN_EXPIRY,
'iat': datetime.utcnow(),
'type': 'access'
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def require_auth(f):
"""Decorator to protect routes"""
@wraps(f)
def decorated_function(*args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({'error': 'Authentication required'}), 401
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
request.user_id = payload['user_id']
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 401
return f(*args, **kwargs)
return decorated_function
# Usage
@app.route('/api/profile')
@require_auth
def get_profile():
user = User.query.get(request.user_id)
return jsonify(user.to_dict())
✓ Implement Proper Authorization
Authentication confirms who you are. Authorization determines what you can do. Always verify that the authenticated user has permission to access the requested resource.
def check_resource_access(user_id: str, resource_id: str) -> bool:
"""Verify user has permission to access resource"""
resource = Resource.query.get(resource_id)
if not resource:
return False
# Check ownership
if resource.owner_id == user_id:
return True
# Check team membership
if user_id in resource.shared_with:
return True
return False
@app.route('/api/documents/<doc_id>')
@require_auth
def get_document(doc_id):
# ❌ WRONG: Just checking authentication
# document = Document.query.get(doc_id)
# ✓ CORRECT: Checking authorization
if not check_resource_access(request.user_id, doc_id):
return jsonify({'error': 'Access denied'}), 403
document = Document.query.get(doc_id)
return jsonify(document.to_dict())
2. Input Validation & Sanitization
✓ Validate All Input
Never trust client input. Validate data types, formats, and ranges before processing.
from pydantic import BaseModel, validator, EmailStr
from typing import Optional
class CreateUserRequest(BaseModel):
email: EmailStr
username: str
age: int
website: Optional[str] = None
@validator('username')
def username_alphanumeric(cls, v):
if not v.isalnum():
raise ValueError('Username must be alphanumeric')
if len(v) < 3 or len(v) > 20:
raise ValueError('Username must be 3-20 characters')
return v
@validator('age')
def age_range(cls, v):
if v < 13 or v > 120:
raise ValueError('Invalid age')
return v
@validator('website')
def validate_url(cls, v):
if v and not v.startswith(('http://', 'https://')):
raise ValueError('Invalid URL format')
return v
@app.route('/api/users', methods=['POST'])
@require_auth
def create_user():
try:
# Validation happens automatically
user_data = CreateUserRequest(**request.json)
# Safe to use validated data
new_user = User(
email=user_data.email,
username=user_data.username,
age=user_data.age
)
db.session.add(new_user)
db.session.commit()
return jsonify(new_user.to_dict()), 201
except ValidationError as e:
return jsonify({'errors': e.errors()}), 400
✓ Prevent SQL Injection
Always use parameterized queries or an ORM. Never concatenate user input into SQL strings.
# ❌ VULNERABLE to SQL Injection
@app.route('/api/users/search')
def search_users():
query = request.args.get('q')
sql = f"SELECT * FROM users WHERE name LIKE '%{query}%'"
results = db.execute(sql)
# Attacker can inject: ' OR '1'='1
# ✓ SAFE: Using parameterized query
@app.route('/api/users/search')
def search_users():
query = request.args.get('q')
results = User.query.filter(User.name.like(f'%{query}%')).all()
# ORM handles escaping automatically
# ✓ SAFE: Using raw SQL with parameters
@app.route('/api/users/search')
def search_users():
query = request.args.get('q')
sql = "SELECT * FROM users WHERE name LIKE :search"
results = db.execute(sql, {'search': f'%{query}%'})
# Parameters are safely escaped
3. Rate Limiting & DDoS Protection
✓ Implement Rate Limiting
Protect your API from abuse, brute force attacks, and DDoS by limiting request rates.
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"
)
# Different limits for different endpoints
@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per minute") # Prevent brute force
def login():
# Login logic
pass
@app.route('/api/expensive-operation', methods=['POST'])
@limiter.limit("10 per hour") # Expensive operations
@require_auth
def expensive_operation():
# Expensive logic
pass
# Custom rate limit based on user tier
def get_user_tier():
"""Get rate limit based on user subscription"""
if not hasattr(request, 'user_id'):
return "10 per hour" # Anonymous users
user = User.query.get(request.user_id)
if user.tier == 'premium':
return "1000 per hour"
elif user.tier == 'basic':
return "100 per hour"
return "10 per hour"
@app.route('/api/data')
@limiter.limit(get_user_tier)
@require_auth
def get_data():
# Return data based on user's subscription
pass
4. Data Exposure Prevention
✓ Don't Leak Sensitive Data
Be careful about what data you expose in API responses. Filter out sensitive fields.
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120))
password_hash = db.Column(db.String(255)) # Never expose!
api_key = db.Column(db.String(64)) # Never expose!
created_at = db.Column(db.DateTime)
last_login_ip = db.Column(db.String(45)) # Sensitive!
def to_dict(self, include_sensitive=False):
"""Safe serialization with explicit control"""
data = {
'id': self.id,
'email': self.email,
'created_at': self.created_at.isoformat()
}
# Only include sensitive data when explicitly requested
# and authorized
if include_sensitive:
data['last_login_ip'] = self.last_login_ip
# Never include these, even if requested
# - password_hash
# - api_key
return data
@app.route('/api/users/<user_id>')
@require_auth
def get_user(user_id):
user = User.query.get_or_404(user_id)
# Check if requester is viewing their own profile
is_self = request.user_id == user_id
return jsonify(user.to_dict(include_sensitive=is_self))
5. HTTPS & Transport Security
✓ Enforce HTTPS
Always use HTTPS for all API endpoints. Implement HSTS headers to prevent downgrade attacks.
from flask_talisman import Talisman
# Force HTTPS and set security headers
Talisman(app,
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000, # 1 year
content_security_policy={
'default-src': "'self'",
'script-src': "'self'",
'style-src': "'self'"
}
)
@app.before_request
def before_request():
"""Redirect HTTP to HTTPS"""
if not request.is_secure and app.env != 'development':
url = request.url.replace('http://', 'https://', 1)
return redirect(url, code=301)
6. API Logging & Monitoring
✓ Log Security Events
Comprehensive logging helps detect and respond to security incidents.
import logging
from datetime import datetime
security_logger = logging.getLogger('security')
def log_security_event(event_type, user_id=None, details=None):
"""Log security-relevant events"""
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'event_type': event_type,
'user_id': user_id,
'ip_address': request.remote_addr,
'user_agent': request.headers.get('User-Agent'),
'details': details
}
security_logger.warning(json.dumps(log_entry))
@app.route('/api/login', methods=['POST'])
def login():
email = request.json.get('email')
password = request.json.get('password')
user = User.query.filter_by(email=email).first()
if not user or not user.check_password(password):
log_security_event(
'LOGIN_FAILED',
user_id=user.id if user else None,
details={'email': email}
)
return jsonify({'error': 'Invalid credentials'}), 401
log_security_event(
'LOGIN_SUCCESS',
user_id=user.id
)
return jsonify({'token': create_access_token(user.id)})
# Monitor for brute force attempts
@app.before_request
def check_brute_force():
ip = request.remote_addr
recent_failures = SecurityLog.query.filter(
SecurityLog.ip_address == ip,
SecurityLog.event_type == 'LOGIN_FAILED',
SecurityLog.timestamp > datetime.utcnow() - timedelta(minutes=5)
).count()
if recent_failures > 10:
log_security_event('BRUTE_FORCE_DETECTED', details={'ip': ip})
return jsonify({'error': 'Too many failed attempts'}), 429
7. API Versioning & Deprecation
✓ Version Your API
Proper versioning allows you to fix security issues without breaking existing clients.
# Version in URL path
@app.route('/api/v1/users')
def get_users_v1():
# Old, deprecated version
pass
@app.route('/api/v2/users')
def get_users_v2():
# New, secure version with additional validation
pass
# Or version in header
@app.before_request
def check_api_version():
api_version = request.headers.get('API-Version', 'v1')
if api_version == 'v1':
# Warn about deprecation
response.headers['Warning'] = '299 - "API v1 deprecated. Upgrade to v2"'
# Block severely outdated versions
if api_version < 'v1':
return jsonify({'error': 'API version no longer supported'}), 410
Quick Security Checklist
Use this checklist before deploying your API:
- ☑ Authentication implemented with JWT or OAuth 2.0
- ☑ Authorization checks on every endpoint
- ☑ Input validation with a validation library
- ☑ Parameterized queries (no SQL injection)
- ☑ Rate limiting configured
- ☑ HTTPS enforced with HSTS
- ☑ Sensitive data filtered from responses
- ☑ Security logging implemented
- ☑ Error messages don't leak system info
- ☑ CORS configured correctly
- ☑ API keys rotated regularly
- ☑ Dependencies updated and scanned for vulnerabilities
Conclusion
API security is not optional, and it's not something you can "add later." Building security into your API from day one saves time, money, and reputation in the long run.
This checklist covers the fundamentals, but security is an ongoing process. Regular security audits, penetration testing, and staying updated on new vulnerabilities are essential for maintaining a secure API.
Want an Expert Review? Our team can audit your API for security vulnerabilities and provide detailed remediation guidance. Schedule a free security consultation today.