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.