Api Key Exposure in Django
How Api Key Exposure Manifests in Django
API key exposure in Django applications often occurs through configuration files, environment variable mishandling, and improper logging practices. The most common vulnerability appears in settings.py files where developers hardcode API keys directly into the source code. For example:
# settings.py - INSECURE
API_KEY = "sk-1234567890abcdef"
SECRET_KEY = "django-insecure-secret-key-here"
This pattern is particularly dangerous because Django settings files are frequently committed to version control systems. When these files are pushed to public repositories, API keys become immediately accessible to attackers.
Another Django-specific manifestation occurs through the django.core.signing module when developers use weak signing secrets. The default SECRET_KEY value in Django's settings.py is often left unchanged, creating predictable signing keys:
# settings.py - INSECURE
SECRET_KEY = "django-insecure-default-secret"
Environment variable handling in Django also creates exposure risks. Developers frequently use os.environ.get() without proper validation:
# views.py - INSECURE
import os
from django.http import JsonResponse
def external_api_call(request):
api_key = os.environ.get('EXTERNAL_API_KEY')
# No validation if api_key is None
response = requests.get(
'https://api.example.com/data',
headers={'Authorization': f'Bearer {api_key}'}
)
return JsonResponse(response.json())
Django's logging framework can inadvertently expose API keys when exceptions occur. A common pattern that leads to exposure:
# views.py - INSECURE
import logging
from django.http import JsonResponse
def process_payment(request):
try:
# Payment processing logic
pass
except Exception as e:
logging.error(f"Payment failed: {e}") # Exception might contain API keys
return JsonResponse({'error': 'Payment processing failed'})
Middleware implementations in Django can also leak API keys through improper error responses. When authentication fails or external services are unreachable, stack traces may include sensitive configuration details:
# middleware.py - INSECURE
class ExternalAPIMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.api_key = os.environ.get('EXTERNAL_API_KEY')
def __call__(self, request):
response = self.get_response(request)
if response.status_code == 503:
return JsonResponse({
'error': 'Service unavailable',
'debug': f'API key: {self.api_key}' # DIRECT EXPOSURE
})
return response
Django-Specific Detection
Detecting API key exposure in Django applications requires examining both the codebase and runtime behavior. Start with static analysis of your settings.py and configuration files:
Static Code Analysis
# Check for hardcoded API keys in settings.py
grep -r "sk-[0-9a-f]\{24\}" . --include="*.py"
grep -r "sk_[0-9a-zA-Z]\{20,\}" . --include="*.py"
grep -r "Bearer [A-Za-z0-9+/=]\-" . --include="*.py"
Environment Variable Validation
# management/commands/check_api_keys.py
from django.core.management.base import BaseCommand
import os
def validate_api_key(key):
if key is None:
return False, "API key not set"
if len(key) < 20: # Arbitrary minimum length
return False, "API key too short"
if "" in key or " " in key:
return False, "API key contains spaces"
return True, "Valid API key"
class Command(BaseCommand):
help = "Check for API key exposure vulnerabilities"
def handle(self, *args, **options):
required_keys = ['EXTERNAL_API_KEY', 'PAYMENT_API_KEY']
for key in required_keys:
value = os.environ.get(key)
if value is None:
self.stdout.write(self.style.ERROR(f"Missing {key}"))
continue
is_valid, message = validate_api_key(value)
if not is_valid:
self.stdout.write(self.style.WARNING(f"{key}: {message}"))
else:
self.stdout.write(self.style.SUCCESS(f"{key}: {message}"))
middleBrick Security Scanning
middleBrick provides automated API key exposure detection specifically for Django applications. The scanner examines your API endpoints for:
- Response headers containing API keys
- Error responses with sensitive configuration data
- Authentication bypass attempts that might reveal key usage patterns
- Log file exposure through misconfigured endpoints
To scan a Django API with middleBrick:
# Install middleBrick CLI
npm install -g middlebrick
# Scan your Django API
middlebrick scan https://your-django-app.com/api/v1/
The scanner tests unauthenticated endpoints for BOLA (Broken Object Level Authorization) vulnerabilities that could expose API keys through IDOR attacks. It also checks for:
- Missing authentication on sensitive endpoints
- Excessive data exposure in API responses
- Improper error handling that reveals implementation details
Runtime Detection
# middleware.py - Secure version with detection
import logging
from django.http import JsonResponse
class APISecurityMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.logger = logging.getLogger(__name__)
def __call__(self, request):
# Check for API key in response
response = self.get_response(request)
# Scan response content for potential API key exposure
if hasattr(response, 'content'):
content = response.content.decode('utf-8', errors='ignore')
if self.contains_api_key(content):
self.logger.warning(
f"Potential API key exposure detected: {request.path}"
)
return response
def contains_api_key(self, content):
# Simple pattern matching for common API key formats
patterns = [
r'sk-[0-9a-f]{24}',
r'Bearer [A-Za-z0-9+/=]{20,}',
r'api_key: [A-Za-z0-9]{20,}'
]
for pattern in patterns:
if re.search(pattern, content):
return True
return False
Django-Specific Remediation
Securing API keys in Django requires a multi-layered approach using Django's built-in security features and external secret management services. Here's how to implement comprehensive protection:
Environment Variable Management
# settings.py - SECURE
import os
from django.core.exceptions import ImproperlyConfigured
def get_env_variable(var_name):
"""Securely retrieve environment variables with validation"""
try:
value = os.environ[var_name]
if not value:
raise KeyError
return value
except KeyError:
error_msg = f"Set the {var_name} environment variable"
raise ImproperlyConfigured(error_msg)
# Load API keys with validation
EXTERNAL_API_KEY = get_env_variable('EXTERNAL_API_KEY')
PAYMENT_API_KEY = get_env_variable('PAYMENT_API_KEY')
# Validate key format
import re
if not re.match(r'^[A-Za-z0-9_/-]{20,}$', EXTERNAL_API_KEY):
raise ImproperlyConfigured(
'EXTERNAL_API_KEY format is invalid'
)
Secret Management with django-environ
# settings.py - Using django-environ
import environ
env = environ.Env(
# Define expected types for validation
EXTERNAL_API_KEY=(str, 'No external API key set'),
PAYMENT_API_KEY=(str, 'No payment API key set'),
DEBUG=(bool, False),
)
# Read .env file if it exists
env.read_env('.env')
# Access with type validation
EXTERNAL_API_KEY = env('EXTERNAL_API_KEY')
PAYMENT_API_KEY = env('PAYMENT_API_KEY')
# Optional: Use a secrets manager
if 'AWS_SECRETS_MANAGER' in os.environ:
import boto3
from botocore.exceptions import ClientError
client = boto3.client('secretsmanager')
try:
response = client.get_secret_value(
SecretId='django-api-keys'
)
secrets = json.loads(response['SecretString'])
EXTERNAL_API_KEY = secrets['external_api_key']
except ClientError as e:
raise ImproperlyConfigured(
f"Failed to retrieve secrets: {e.response['Error']['Message']}"
)
Secure Middleware Implementation
# middleware.py - Secure version
import logging
import re
from django.http import JsonResponse
from django.conf import settings
class APISecurityMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.logger = logging.getLogger(__name__)
self.api_key_patterns = [
re.compile(r'sk-[0-9a-f]{24}'),
re.compile(r'Bearer [A-Za-z0-9+/=]{20,}'),
re.compile(r'api_key: [A-Za-z0-9]{20,}'),
]
def __call__(self, request):
response = self.get_response(request)
# Check response for potential API key exposure
if hasattr(response, 'content'):
content = response.content.decode('utf-8', errors='ignore')
if self.detect_api_keys(content):
self.logger.warning(
f"Potential API key exposure detected: {request.path}"
)
# Sanitize response content
sanitized_content = self.sanitize_content(content)
response.content = sanitized_content.encode('utf-8')
return response
def detect_api_keys(self, content):
for pattern in self.api_key_patterns:
if pattern.search(content):
return True
return False
def sanitize_content(self, content):
# Replace potential API keys with placeholders
for pattern in self.api_key_patterns:
content = pattern.sub('[REDACTED_API_KEY]', content)
return content
Secure View Implementation
# views.py - SECURE
import logging
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
logger = logging.getLogger(__name__)
@require_http_methods(["POST"])
@csrf_exempt
def process_payment(request):
try:
# Validate request data
data = json.loads(request.body)
if not data.get('amount') or not data.get('currency'):
return JsonResponse({
'error': 'Invalid payment data'
}, status=400)
# Call external API with validated key
response = call_external_payment_api(
amount=data['amount'],
currency=data['currency']
)
return JsonResponse(response.json())
except Exception as e:
# Log without exposing sensitive data
logger.error(f"Payment processing failed: {str(e)[:200]}") # Truncate
return JsonResponse({
'error': 'Payment processing failed'
}, status=500)
def call_external_payment_api(amount, currency):
"""Securely call external payment API"""
import requests
# Use Django settings for API key
headers = {
'Authorization': f'Bearer {settings.PAYMENT_API_KEY}',
'Content-Type': 'application/json'
}
payload = {
'amount': amount,
'currency': currency,
'description': 'Django payment processing'
}
try:
response = requests.post(
'https://api.paymentprovider.com/v1/payments',
json=payload,
headers=headers,
timeout=10
)
response.raise_for_status()
return response
except requests.exceptions.RequestException as e:
logger.error(f"Payment API error: {str(e)}")
raise
Logging Configuration
# logging.py - SECURE
import logging
import re
from django.utils.log import AdminEmailHandler
class APISecureLoggingHandler(AdminEmailHandler):
def emit(self, record):
"""Filter sensitive data from log messages"""
message = record.getMessage()
# Redact potential API keys
patterns = [
r'sk-[0-9a-f]{24}',
r'Bearer [A-Za-z0-9+/=]{20,}',
r'api_key: [A-Za-z0-9]{20,}',
]
for pattern in patterns:
message = re.sub(pattern, '[REDACTED]', message)
record.message = message
super().emit(record)
# Configure logging in settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'secure_console': {
'class': 'APISecureLoggingHandler',
'include_html': False,
},
'secure_email': {
'class': 'APISecureLoggingHandler',
'include_html': False,
},
},
'loggers': {
'django': {
'handlers': ['secure_console', 'secure_email'],
'level': 'WARNING',
},
},
}