Django Path Traversal via write() with Request Data

Critical Risk Path Traversal
djangopythonpath-traversalfile-writearbitrary-file-writerequest-data

What it is

The Django application uses file write operations with user-controlled paths or content from request data without proper validation, enabling path traversal attacks and arbitrary file write vulnerabilities. Attackers can manipulate file paths and content to write files outside the intended directory structure, potentially overwriting critical system files, configuration files, or injecting malicious content into application files.

# Vulnerable: File write with user-controlled paths and content

from django.http import JsonResponse
from django.views import View
from django.conf import settings
import os

# Dangerous: Direct file write with request data
class FileWriteView(View):
    def post(self, request):
        filename = request.POST.get('filename', '')
        content = request.POST.get('content', '')
        directory = request.POST.get('directory', '')
        
        # CRITICAL: User controls file path and content
        if directory:
            file_path = os.path.join(settings.MEDIA_ROOT, directory, filename)
        else:
            file_path = os.path.join(settings.MEDIA_ROOT, 'uploads', filename)
        
        try:
            with open(file_path, 'w') as f:
                f.write(content)
            
            return JsonResponse({'status': 'File written successfully'})
        except Exception as e:
            return JsonResponse({'error': str(e)})

# Another dangerous pattern
def save_user_config(request):
    config_name = request.POST.get('config_name', '')
    config_data = request.POST.get('config_data', '')
    user_id = request.POST.get('user_id', '')
    
    # Dangerous: User controls config path and content
    config_path = f'/var/app/configs/{user_id}/{config_name}.conf'
    
    try:
        os.makedirs(os.path.dirname(config_path), exist_ok=True)
        with open(config_path, 'w') as f:
            f.write(config_data)
        
        return JsonResponse({'status': 'Config saved'})
    except Exception as e:
        return JsonResponse({'error': str(e)})

# Log file creation
def create_log_entry(request):
    log_name = request.GET.get('log', '')
    log_content = request.POST.get('content', '')
    date = request.GET.get('date', '')
    
    # Dangerous: User-controlled log path
    log_path = f'/var/log/app/{date}/{log_name}.log'
    
    try:
        os.makedirs(os.path.dirname(log_path), exist_ok=True)
        with open(log_path, 'a') as f:
            f.write(log_content + '\n')
        
        return JsonResponse({'status': 'Log entry created'})
    except Exception as e:
        return JsonResponse({'error': str(e)})

# Template file creation
def save_template(request):
    template_name = request.POST.get('template_name', '')
    template_content = request.POST.get('template_content', '')
    template_type = request.POST.get('type', '')
    
    # Dangerous: User controls template path and content
    template_path = os.path.join(settings.BASE_DIR, 'templates', template_type, template_name)
    
    try:
        os.makedirs(os.path.dirname(template_path), exist_ok=True)
        with open(template_path, 'w') as f:
            f.write(template_content)
        
        return JsonResponse({'status': 'Template saved'})
    except Exception as e:
        return JsonResponse({'error': str(e)})

# File upload with custom path
def upload_file_custom_path(request):
    if 'file' not in request.FILES:
        return JsonResponse({'error': 'No file uploaded'})
    
    uploaded_file = request.FILES['file']
    custom_path = request.POST.get('path', '')
    filename = request.POST.get('filename', uploaded_file.name)
    
    # Dangerous: User controls file path
    full_path = os.path.join(settings.MEDIA_ROOT, custom_path, filename)
    
    try:
        os.makedirs(os.path.dirname(full_path), exist_ok=True)
        with open(full_path, 'wb') as f:
            for chunk in uploaded_file.chunks():
                f.write(chunk)
        
        return JsonResponse({'status': 'File uploaded', 'path': full_path})
    except Exception as e:
        return JsonResponse({'error': str(e)})

# Backup file creation
def create_backup_file(request):
    backup_name = request.POST.get('backup_name', '')
    backup_data = request.POST.get('data', '')
    backup_dir = request.POST.get('directory', 'daily')
    
    # Dangerous: User controls backup path
    backup_path = f'/backups/{backup_dir}/{backup_name}.bak'
    
    try:
        with open(backup_path, 'w') as f:
            f.write(backup_data)
        
        return JsonResponse({'status': 'Backup created'})
    except Exception as e:
        return JsonResponse({'error': str(e)})
# Secure: Safe file write operations in Django

from django.http import JsonResponse
from django.views import View
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from pathlib import Path
import os
import re
import json

# Safe: Validated file write
class SafeFileWriteView(View):
    def post(self, request):
        filename = request.POST.get('filename', '')
        content = request.POST.get('content', '')
        
        try:
            # Validate inputs
            validated_data = self.validate_file_write_data(filename, content)
            
            # Write file safely
            file_path = self.write_file_safely(validated_data)
            
            return JsonResponse({
                'status': 'File written successfully',
                'filename': validated_data['filename']
            })
        except ValidationError as e:
            return JsonResponse({'error': str(e)}, status=400)
    
    def validate_file_write_data(self, filename, content):
        # Validate filename
        if not filename or len(filename) > 100:
            raise ValidationError('Invalid filename length')
        
        # Only allow safe characters in filename
        if not re.match(r'^[a-zA-Z0-9._-]+$', filename):
            raise ValidationError('Filename contains invalid characters')
        
        # Prevent hidden files and traversal
        if filename.startswith('.') or '..' in filename:
            raise ValidationError('Invalid filename format')
        
        # Check file extension
        allowed_extensions = ['.txt', '.json', '.csv', '.log']
        if not any(filename.lower().endswith(ext) for ext in allowed_extensions):
            raise ValidationError('File extension not allowed')
        
        # Validate content
        if not content:
            raise ValidationError('Content cannot be empty')
        
        if len(content) > 1024 * 1024:  # 1MB limit
            raise ValidationError('Content too large')
        
        # Basic content validation
        try:
            content.encode('utf-8')
        except UnicodeEncodeError:
            raise ValidationError('Content contains invalid characters')
        
        return {'filename': filename, 'content': content}
    
    def write_file_safely(self, data):
        # Define safe base directory
        upload_dir = Path(settings.MEDIA_ROOT) / 'user_uploads'
        upload_dir.mkdir(exist_ok=True)
        
        # Construct safe file path
        file_path = upload_dir / data['filename']
        
        # Validate path is within upload directory
        try:
            resolved_path = file_path.resolve()
            upload_dir_resolved = upload_dir.resolve()
            
            resolved_path.relative_to(upload_dir_resolved)
        except ValueError:
            raise ValidationError('File path outside allowed directory')
        
        # Check if file already exists
        if resolved_path.exists():
            raise ValidationError('File already exists')
        
        # Write file
        try:
            with open(resolved_path, 'w', encoding='utf-8') as f:
                f.write(data['content'])
            
            return resolved_path
        except PermissionError:
            raise ValidationError('Permission denied')
        except Exception:
            raise ValidationError('File write error')

# Safe: Configuration management
def safe_save_user_config(request):
    config_type = request.POST.get('config_type', '')
    config_data = request.POST.get('config_data', '')
    
    try:
        # Validate inputs
        validated_config = validate_config_data(config_type, config_data)
        
        # Save configuration safely
        save_config_safely(request.user, validated_config)
        
        return JsonResponse({'status': 'Configuration saved'})
    except ValidationError as e:
        return JsonResponse({'error': str(e)}, status=400)

def validate_config_data(config_type, config_data):
    # Validate config type
    allowed_types = ['theme', 'notifications', 'privacy', 'display']
    if config_type not in allowed_types:
        raise ValidationError('Configuration type not allowed')
    
    # Validate config data
    try:
        config_obj = json.loads(config_data)
    except json.JSONDecodeError:
        raise ValidationError('Invalid JSON format')
    
    # Validate config structure based on type
    if config_type == 'theme':
        allowed_keys = {'color_scheme', 'font_size', 'layout'}
        allowed_values = {
            'color_scheme': ['light', 'dark', 'auto'],
            'font_size': ['small', 'medium', 'large'],
            'layout': ['compact', 'comfortable', 'spacious']
        }
    elif config_type == 'notifications':
        allowed_keys = {'email_notifications', 'push_notifications', 'frequency'}
        allowed_values = {
            'email_notifications': [True, False],
            'push_notifications': [True, False],
            'frequency': ['immediate', 'daily', 'weekly']
        }
    # Add other config type validations...
    
    # Validate keys and values
    validated_config = {}
    for key, value in config_obj.items():
        if key in allowed_keys and value in allowed_values.get(key, [value]):
            validated_config[key] = value
    
    return {'type': config_type, 'data': validated_config}

def save_config_safely(user, config_data):
    # Use Django model to save configuration
    from .models import UserConfiguration
    
    config, created = UserConfiguration.objects.get_or_create(
        user=user,
        config_type=config_data['type'],
        defaults={'config_data': config_data['data']}
    )
    
    if not created:
        config.config_data = config_data['data']
        config.save()

# Safe: Log entry creation
def safe_create_log_entry(request):
    log_type = request.POST.get('log_type', '')
    message = request.POST.get('message', '')
    
    try:
        # Validate inputs
        validated_log = validate_log_entry(log_type, message)
        
        # Create log entry safely
        create_log_entry_safely(request.user, validated_log)
        
        return JsonResponse({'status': 'Log entry created'})
    except ValidationError as e:
        return JsonResponse({'error': str(e)}, status=400)

def validate_log_entry(log_type, message):
    # Validate log type
    allowed_types = ['user_action', 'system_event', 'error', 'security']
    if log_type not in allowed_types:
        raise ValidationError('Log type not allowed')
    
    # Validate message
    if not message or len(message) > 1000:
        raise ValidationError('Invalid message length')
    
    # Sanitize message
    clean_message = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', message)
    
    return {'type': log_type, 'message': clean_message}

def create_log_entry_safely(user, log_data):
    # Use Django logging framework
    import logging
    
    logger = logging.getLogger(f'app.{log_data["type"]}')
    
    log_message = f'User {user.id} ({user.username}): {log_data["message"]}'
    
    if log_data['type'] == 'error':
        logger.error(log_message)
    elif log_data['type'] == 'security':
        logger.warning(log_message)
    else:
        logger.info(log_message)

# Safe: Template management
def safe_save_template(request):
    template_id = request.POST.get('template_id', '')
    template_content = request.POST.get('content', '')
    
    try:
        # Validate inputs
        validated_template = validate_template_data(template_id, template_content)
        
        # Save template safely
        save_template_safely(request.user, validated_template)
        
        return JsonResponse({'status': 'Template saved'})
    except ValidationError as e:
        return JsonResponse({'error': str(e)}, status=400)

def validate_template_data(template_id, content):
    # Validate template ID
    if not template_id.isdigit():
        raise ValidationError('Invalid template ID')
    
    template_id = int(template_id)
    
    # Validate content
    if not content or len(content) > 10000:
        raise ValidationError('Invalid template content length')
    
    # Basic HTML validation (remove dangerous elements)
    import html
    clean_content = html.escape(content)
    
    return {'id': template_id, 'content': clean_content}

def save_template_safely(user, template_data):
    # Use Django model to save template
    from .models import UserTemplate
    
    try:
        template = UserTemplate.objects.get(
            id=template_data['id'],
            user=user
        )
        template.content = template_data['content']
        template.save()
    except UserTemplate.DoesNotExist:
        raise ValidationError('Template not found or access denied')

# Safe: File upload with validation
def safe_upload_file(request):
    if 'file' not in request.FILES:
        return JsonResponse({'error': 'No file uploaded'}, status=400)
    
    uploaded_file = request.FILES['file']
    file_category = request.POST.get('category', '')
    
    try:
        # Validate file and category
        validated_upload = validate_file_upload(uploaded_file, file_category)
        
        # Save file safely
        file_path = save_uploaded_file_safely(request.user, validated_upload)
        
        return JsonResponse({
            'status': 'File uploaded successfully',
            'filename': validated_upload['filename']
        })
    except ValidationError as e:
        return JsonResponse({'error': str(e)}, status=400)

def validate_file_upload(uploaded_file, category):
    # Validate file size
    max_size = 10 * 1024 * 1024  # 10MB
    if uploaded_file.size > max_size:
        raise ValidationError('File too large')
    
    # Validate filename
    filename = uploaded_file.name
    if not filename or len(filename) > 255:
        raise ValidationError('Invalid filename')
    
    # Sanitize filename
    safe_filename = re.sub(r'[^a-zA-Z0-9._-]', '_', filename)
    
    # Validate file type
    allowed_types = {
        'images': ['image/jpeg', 'image/png', 'image/gif'],
        'documents': ['application/pdf', 'text/plain'],
        'data': ['text/csv', 'application/json']
    }
    
    if category not in allowed_types:
        raise ValidationError('Invalid file category')
    
    if uploaded_file.content_type not in allowed_types[category]:
        raise ValidationError('File type not allowed for this category')
    
    return {
        'file': uploaded_file,
        'filename': safe_filename,
        'category': category
    }

def save_uploaded_file_safely(user, upload_data):
    # Define safe upload directory
    upload_dir = Path(settings.MEDIA_ROOT) / 'user_files' / str(user.id) / upload_data['category']
    upload_dir.mkdir(parents=True, exist_ok=True)
    
    # Generate unique filename
    import uuid
    unique_filename = f"{uuid.uuid4()}_{upload_data['filename']}"
    file_path = upload_dir / unique_filename
    
    # Save file
    uploaded_file = upload_data['file']
    try:
        with open(file_path, 'wb') as f:
            for chunk in uploaded_file.chunks():
                f.write(chunk)
        
        return file_path
    except Exception:
        raise ValidationError('File save error')

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Views use request parameters for file paths in write operations without validation: filename = request.POST['file']; with open(os.path.join(base, filename), 'w') as f: f.write(content). Attackers inject '../' to write anywhere.

Root causes

Using Request Data Directly in File Write Operations

Views use request parameters for file paths in write operations without validation: filename = request.POST['file']; with open(os.path.join(base, filename), 'w') as f: f.write(content). Attackers inject '../' to write anywhere.

Missing Validation of File Paths Before Write Operations

Applications write files without checking paths for traversal sequences. No validation of '..' or absolute paths allows writing to /etc/passwd or overwriting application code through manipulated filenames in requests.

Insufficient Filtering of Directory Traversal Sequences

Weak filtering like path.replace('..', '') fails against nested '..../' patterns or encoded '%2e%2e%2f'. Single-pass replacement leaves traversal sequences that enable writing files outside intended directories.

Trusting User-Provided File Paths and Content Without Verification

Applications trust request data for both paths and content: path = request.POST['path']; content = request.POST['data']; open(path, 'w').write(content). Complete control enables overwriting system files or injecting malicious code.

Direct Use of Request Data in File System Operations

F-string path construction with request data: file_path = f'{upload_dir}/{request.GET["dir"]}/{request.POST["name"]}'. Multiple user-controlled components create numerous injection points for traversal attacks in write operations.

Fixes

1

Validate and Sanitize All File Paths Before Write Operations

Enforce alphanumeric filenames with regex: re.match(r'^[a-zA-Z0-9._-]+$', filename). Reject '..' sequences, limit length to 255 chars, validate extensions against allowlist. Check for hidden files and system names before writing.

2

Use Allowlists for Permitted File Names and Directories

Map identifiers to filenames: ALLOWED = {'report1': 'data.pdf'}; name = ALLOWED.get(id). Never construct paths from direct user input. Store file metadata in database with access controls, validate against allowed entries.

3

Implement Proper Path Resolution and Boundary Checking

Use Path.resolve() then verify with relative_to(): path = (base / name).resolve(); path.relative_to(base). Catches traversal where resolved path escapes base directory. Validate parent directory before writing files.

4

Use Indirect File References Instead of Direct Paths

Generate UUIDs for file storage: file_id = uuid.uuid4(); FileRecord.objects.create(id=file_id, path=actual_path). Users provide UUIDs, application controls paths. Prevents direct path manipulation in write operations.

5

Validate and Sanitize File Content Before Writing

Check content size limits, validate encoding, scan for malicious patterns. For config files, validate JSON/YAML structure. For uploads, verify MIME types match extensions. Reject executable content or scripts.

6

Employ Secure File Upload Mechanisms with Proper Restrictions

Use FileField with upload_to: file = models.FileField(upload_to='uploads/%Y/%m/%d/'). Configure FileSystemStorage with strict location boundaries. Set file permissions (0600) and validate on save() through custom storage backend.

Detect This Vulnerability in Your Code

Sourcery automatically identifies django path traversal via write() with request data and many other security issues in your codebase.