HIGH api key exposuredjango

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',
        },
    },
}

Frequently Asked Questions

How can I test if my Django API keys are properly secured?
Use middleBrick's automated scanning to test your API endpoints for key exposure. The scanner checks for BOLA vulnerabilities, authentication bypass attempts, and improper error handling that might reveal API keys. You can also perform manual testing by attempting unauthenticated access to sensitive endpoints and checking error responses for configuration details.
What's the best way to manage API keys in Django production?
Use environment variables with validation through django-environ or a dedicated secrets manager like AWS Secrets Manager or HashiCorp Vault. Never commit API keys to version control. Implement middleware to detect and redact keys in logs and responses. Use Django's built-in validation to ensure keys meet format requirements before the application starts.