Ruby Rails Insecure send_file Usage Vulnerability (Brakeman Check)

High Risk Path Traversal
RubyRailssend_filePath TraversalBrakemanFile AccessDirectory Traversal

What it is

Application uses Rails send_file method with user-controlled paths, enabling path traversal attacks and unauthorized file access to sensitive system files.

class DownloadsController < ApplicationController def show # Vulnerable: Direct user input to send_file filename = params[:filename] file_path = "/uploads/#{filename}" # Dangerous: No path validation send_file file_path end def attachment # Vulnerable: User controls entire path path = params[:path] # Extremely dangerous: Can access any system file send_file path, disposition: 'attachment' end def document # Vulnerable: Path traversal possible doc_name = params[:document] doc_path = "#{Rails.root}/documents/#{doc_name}" # Can access files outside documents directory send_file doc_path, type: 'application/pdf' end end
class DownloadsController < ApplicationController SAFE_UPLOAD_DIR = Rails.root.join('uploads').freeze SAFE_DOCUMENTS_DIR = Rails.root.join('documents').freeze ALLOWED_FILE_TYPES = %w[.pdf .txt .jpg .png .doc .docx].freeze def validate_filename(filename) return nil if filename.blank? # Remove path components clean_filename = File.basename(filename) # Validate filename format unless clean_filename.match?(/\A[a-zA-Z0-9._-]+\z/) raise ArgumentError, 'Invalid filename format' end # Check length if clean_filename.length > 100 raise ArgumentError, 'Filename too long' end clean_filename end def validate_file_path(filename, base_dir) clean_filename = validate_filename(filename) file_path = base_dir.join(clean_filename) # Resolve and normalize path resolved_path = file_path.realpath base_realpath = base_dir.realpath # Ensure path is within allowed directory unless resolved_path.to_s.start_with?(base_realpath.to_s) raise ArgumentError, 'Path traversal detected' end resolved_path end def validate_file_type(file_path) file_ext = File.extname(file_path).downcase unless ALLOWED_FILE_TYPES.include?(file_ext) raise ArgumentError, "File type #{file_ext} not allowed" end file_ext end def show begin filename = params[:filename] # Secure: Validate and construct safe path file_path = validate_file_path(filename, SAFE_UPLOAD_DIR) unless File.exist?(file_path) return render json: { error: 'File not found' }, status: 404 end # Validate file type validate_file_type(file_path) # Secure file serving send_file file_path, disposition: 'attachment', filename: File.basename(file_path) rescue ArgumentError => e render json: { error: e.message }, status: 400 rescue Errno::ENOENT render json: { error: 'File not found' }, status: 404 rescue => e Rails.logger.error "File serving error: #{e.message}" render json: { error: 'File access failed' }, status: 500 end end def attachment # Secure: Don't allow arbitrary path access render json: { error: 'Direct path access not allowed' }, status: 403 end def document begin doc_name = params[:document] # Secure: Validate against documents directory file_path = validate_file_path(doc_name, SAFE_DOCUMENTS_DIR) unless File.exist?(file_path) return render json: { error: 'Document not found' }, status: 404 end # Validate it's a PDF unless File.extname(file_path).downcase == '.pdf' return render json: { error: 'Only PDF documents allowed' }, status: 403 end # Secure PDF serving send_file file_path, type: 'application/pdf', disposition: 'inline', filename: File.basename(file_path) rescue ArgumentError => e render json: { error: e.message }, status: 400 rescue => e Rails.logger.error "Document serving error: #{e.message}" render json: { error: 'Document access failed' }, status: 500 end end # Alternative: Use X-Sendfile for better performance def secure_download begin filename = params[:filename] file_path = validate_file_path(filename, SAFE_UPLOAD_DIR) unless File.exist?(file_path) return render json: { error: 'File not found' }, status: 404 end validate_file_type(file_path) # Use X-Sendfile header for efficient serving response.headers['X-Sendfile'] = file_path.to_s response.headers['Content-Type'] = 'application/octet-stream' response.headers['Content-Disposition'] = "attachment; filename=\"#{File.basename(file_path)}\"" render body: nil rescue ArgumentError => e render json: { error: e.message }, status: 400 rescue => e Rails.logger.error "Secure download error: #{e.message}" render json: { error: 'Download failed' }, status: 500 end end end

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Controller uses send_file with params: send_file(params[:path]). params[:path] untrusted user input. Path traversal with ../ sequences. Access any file on server. send_file without validation enables arbitrary file downloads. Critical vulnerability in file download features.

Root causes

Using send_file with User-Controlled File Paths

Controller uses send_file with params: send_file(params[:path]). params[:path] untrusted user input. Path traversal with ../ sequences. Access any file on server. send_file without validation enables arbitrary file downloads. Critical vulnerability in file download features.

Not Validating File Paths Before send_file

Missing path validation: filepath = "/var/uploads/#{params[:filename]}"; send_file(filepath). String interpolation with user input. No boundary checks. Filename may contain traversal. Path escapes intended directory. Validation required before file serving.

Using send_file with Dynamic File Selection

Dynamic file serving: file_id = params[:id]; file_path = File.join(UPLOAD_DIR, file_id + '.pdf'); send_file(file_path). file_id from user. Even with extension, traversal possible. ../../../etc/passwd.pdf accesses system files. Path joining doesn't prevent traversal.

send_file Without Authorization Checks

Missing access control: send_file(user_file_path) without checking current_user.id == file.user_id. Horizontal privilege escalation. Users access others' files through ID manipulation. File serving requires both path validation and authorization. Access control essential.

Using Pathname or File.join Without realpath Validation

Path construction without validation: path = Pathname.new(base_dir).join(params[:file]); send_file(path). join doesn't prevent traversal. Symlinks allow escaping. Missing realpath resolution. No verification path within allowed directory after resolution.

Fixes

1

Validate Filenames with Allowlist Pattern

Strict filename validation: filename = params[:file]; raise unless filename =~ /^[a-zA-Z0-9._-]+$/; filepath = Rails.root.join('uploads', filename); send_file(filepath). Allowlist alphanumeric characters. Reject slashes and traversal. basename removes directories. Validation before construction.

2

Use realpath and Verify Path Within Base Directory

Path validation with realpath: base = Rails.root.join('uploads').realpath; full_path = File.join(base, params[:file]).realpath; raise unless full_path.to_s.start_with?(base.to_s); send_file(full_path). realpath resolves symlinks. Boundary check enforces directory restriction. Prevents all traversal.

3

Use Database References Instead of File Paths

Indirect file access: file_id = params[:id]; upload = Upload.find_by(id: file_id, user_id: current_user.id); raise unless upload; send_file(upload.storage_path). Database stores paths. Users reference by ID. Authorization in query. No direct path access.

4

Implement Authorization Before File Access

Access control: file_id = params[:id]; upload = Upload.find(file_id); authorize! :download, upload or raise unless can?(:download, upload); send_file(upload.path). CanCanCan or Pundit authorization. Check permissions. Combine with path validation. Defense-in-depth approach.

5

Use send_data Instead of send_file for Additional Control

Read and send data: filename = secure_filename(params[:file]); filepath = validate_path(filename); data = File.binread(filepath); send_data(data, filename: File.basename(filepath), disposition: 'attachment'). send_data with validated data. Full control over content. Additional processing possible. Sanitize filename.

6

Run Brakeman to Detect Unsafe send_file Usage

Use Brakeman scanner: brakeman -A detects send_file vulnerabilities. Review File Access warnings. Fix flagged instances. Automated detection in CI/CD. Regular scans prevent unsafe file serving. Brakeman specifically checks send_file with user input.

Detect This Vulnerability in Your Code

Sourcery automatically identifies ruby rails insecure send_file usage vulnerability (brakeman check) and many other security issues in your codebase.