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.