Phishing Api Keys in Django
How Phishing API Keys Manifests in Django
In Django applications, "phishing" API keys typically refers to accidental exposure of secret keys through various code paths, making them discoverable by attackers. This is a form of Data Exposure (OWASP API Top 10 A05:2021) and often leads to credential compromise.
Common Django-specific patterns include:
- DEBUG Mode Error Pages: When
DEBUG = Truein production (a frequent misconfiguration), Django's technical 500 error page displays full settings, including any API keys defined insettings.py. For example, ifSTRIPE_API_KEY = 'sk_live_...'is in settings, it becomes visible in the HTML response.# vulnerable settings.py DEBUG = True # Never in production! STRIPE_API_KEY = 'sk_live_51Mz...' # Exposed on error page - Logging Sensitive Data: Using Python's
print()or Django's logging to output request/response objects that contain API keys. For instance, a view that logs the entire request body:# vulnerable view import logging logger = logging.getLogger(__name__) def my_view(request): logger.debug(f"Request body: {request.body}") # If body contains API key, it's logged ... - Django REST Framework (DRF) Serializers: Including API key fields in serializers that are then rendered in API responses. A
ModelSerializerthat inadvertently exposes aapi_keymodel field:# vulnerable serializer class ServiceSerializer(serializers.ModelSerializer): class Meta: model = Service fields = ['id', 'name', 'api_key'] # api_key sent in JSON response - Hardcoded Keys in Views or Utilities: Directly embedding keys in code that may be served statically or leak via error traces.
These patterns create an unauthenticated attack surface where an attacker can simply request an endpoint or trigger an error to harvest valid API keys, then use them to impersonate the application.
Django-Specific Detection
Identifying exposed API keys requires scanning both runtime behavior (HTTP responses) and code/configuration artifacts. middleBrick automates this via its Data Exposure check, which specifically scans for credential leakage in API responses.
Manual detection steps:
- Search codebase for common key patterns (e.g.,
sk_live_,rk_live_,Bearerprefixes) usinggrep -r "sk_live_" .. - Test endpoints in production-like environments with
DEBUG = Falseto ensure no keys leak in custom error pages. - Inspect DRF responses via
curlor browser dev tools to verify no sensitive fields are serialized.
Automated scanning with middleBrick:
middleBrick's black-box scanner submits requests to the target Django API and analyzes all HTTP responses (including error pages) for regex patterns matching common API key formats (covering Stripe, OpenAI, AWS, etc.). It also parses OpenAPI specifications (if provided) to identify parameters that might be expected to contain secrets.
Example CLI scan:
middlebrick scan https://api.example.comSample output snippet highlighting a finding:
{
"risk_score": 65,
"grade": "D",
"findings": [
{
"category": "Data Exposure",
"severity": "high",
"title": "API key exposed in JSON response",
"details": "Endpoint GET /api/v1/services/ returns 'api_key' field in response body",
"remediation": "Remove 'api_key' from DRF serializer fields. Use write_only fields for secrets."
}
]
}This approach catches exposure regardless of whether it originates from DEBUG errors, logging, or serializer misconfiguration, providing a prioritized report with remediation guidance.
Django-Specific Remediation
Fixing API key exposure in Django involves leveraging built-in features and secure patterns. The goal is to ensure secrets never appear in logs, responses, or error pages.
1. Store Secrets in Environment Variables (Never in Code)
Use django-environ or Python's os.getenv to load keys from the environment. settings.py should reference, not define, secrets.
# secure settings.py
import os
from dotenv import load_dotenv
load_dotenv()
DEBUG = os.getenv('DEBUG', 'False') == 'True'
STRIPE_API_KEY = os.getenv('STRIPE_API_KEY') # No hardcoded value
# Ensure DEBUG=False in production (use env var or separate settings module)2. Custom Error Views to Suppress Settings Leakage
Override Django's default error views to return generic responses, preventing any settings from being printed when DEBUG = False (and absolutely never with DEBUG = True in production).
# urls.py
from django.views.defaults import server_error, page_not_found
handler500 = 'myapp.views.custom_500'
handler404 = 'myapp.views.custom_404'
# views.py
from django.shortcuts import render
def custom_500(request):
return render(request, '500.html', status=500) # Template contains no debug data
def custom_404(request, exception):
return render(request, '404.html', status=404)3. Filter Sensitive Data from Logs
Implement a logging filter to scrub API keys from log records before they are written.
# logging_filters.py
import re
class SensitiveDataFilter(logging.Filter):
def filter(self, record):
if hasattr(record, 'msg'):
# Mask common key patterns (e.g., sk_live_...)
record.msg = re.sub(r'(sk_live_|sk_test_| Bearer )\S+', '***REDACTED***', str(record.msg))
return True
# settings.py
LOGGING = {
'filters': {
'sensitive_data': {
'()': 'myapp.logging_filters.SensitiveDataFilter',
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'filters': ['sensitive_data'],
}
}
}4. Secure DRF Serializers
Exclude secret fields from serialization or mark them as write_only.
# serializers.py
class ServiceSerializer(serializers.ModelSerializer):
api_key = serializers.CharField(write_only=True) # Never returned in GET responses
class Meta:
model = Service
fields = ['id', 'name', 'api_key'] # api_key only used on input
# Alternatively, exclude completely if not needed in API:
# fields = ['id', 'name']5. Middleware to Scrub Response Bodies (Defense in Depth)
Add middleware to remove key-like strings from HTTP responses before they reach the client.
# middleware.py
import re
class ScrubSecretsMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if hasattr(response, 'content') and response.content:
# Simple pattern; extend with your key formats
pattern = rb'(sk_live_|sk_test_| Bearer )\S+'
response.content = re.sub(pattern, b'***REDACTED***', response.content)
return response
# settings.py
MIDDLEWARE = [
...,
'myapp.middleware.ScrubSecretsMiddleware',
]These remediation steps align with middleBrick's guidance for the Data Exposure category. After implementing fixes, re-scan with middleBrick to verify the finding is resolved and your risk score improves.
FAQ
- How does middleBrick detect exposed API keys in Django apps?
middleBrick sends HTTP requests to your Django API endpoints and scans all responses (including error pages) using regex patterns for common API key formats (Stripe, AWS, OpenAI, etc.). It also analyzes OpenAPI specifications to identify parameters that might contain secrets, flagging any occurrence in response bodies, headers, or error messages as a Data Exposure finding. - What's the best practice for storing API keys in Django?
Never hardcode API keys insettings.pyor any source file. Use environment variables (viadjango-environoros.getenv) and ensureDEBUG = Falsein production. Additionally, implement logging filters and DRFwrite_onlyfields to prevent accidental leakage via logs or API responses.