Ruby Rails Tainted File Access Vulnerability

High Risk Path Traversal
RubyRailsPath TraversalFile AccessDirectory TraversalInput Validation

What it is

Application uses user-controlled input to access files in Rails applications without proper validation, leading to path traversal attacks and unauthorized file access.

class FilesController < ApplicationController
  def show
    # Vulnerable: Direct user input in file path
    filename = params[:filename]
    file_path = "#{Rails.root}/uploads/#{filename}"
    
    # Dangerous: No path validation
    if File.exist?(file_path)
      send_file file_path
    else
      render plain: 'File not found', status: 404
    end
  end
  
  def download
    # Vulnerable: User controls file path
    path = params[:path]
    
    # Extremely dangerous: Can access any file
    file_content = File.read(path)
    render plain: file_content
  end
  
  def read_config
    # Vulnerable: Config file access
    config_name = params[:config]
    config_path = "/etc/#{config_name}.conf"
    
    # Can read sensitive system files
    File.open(config_path, 'r') do |file|
      render json: { config: file.read }
    end
  end
end
class FilesController < ApplicationController
  ALLOWED_UPLOAD_DIR = Rails.root.join('uploads').freeze
  ALLOWED_CONFIG_FILES = %w[app database].freeze
  
  def validate_filename(filename)
    return nil if filename.blank?
    
    # Remove any path traversal attempts
    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 = ALLOWED_UPLOAD_DIR)
    clean_filename = validate_filename(filename)
    file_path = base_dir.join(clean_filename)
    
    # Ensure path is within allowed directory
    unless file_path.to_s.start_with?(base_dir.to_s)
      raise ArgumentError, 'Path traversal detected'
    end
    
    file_path
  end
  
  def show
    begin
      filename = params[:filename]
      
      # Secure: Validate and construct safe path
      file_path = validate_file_path(filename)
      
      unless File.exist?(file_path)
        return render json: { error: 'File not found' }, status: 404
      end
      
      # Validate file type
      allowed_types = %w[.txt .pdf .jpg .png .doc .docx]
      file_ext = File.extname(file_path).downcase
      
      unless allowed_types.include?(file_ext)
        return render json: { error: 'File type not allowed' }, status: 403
      end
      
      # Secure file serving
      send_file file_path, disposition: 'attachment'
      
    rescue ArgumentError => e
      render json: { error: e.message }, status: 400
    rescue => e
      Rails.logger.error "File access error: #{e.message}"
      render json: { error: 'File access failed' }, status: 500
    end
  end
  
  def download
    # Secure: Don't allow arbitrary path access
    render json: { error: 'Direct path access not allowed' }, status: 403
  end
  
  def read_config
    begin
      config_name = params[:config]
      
      # Secure: Allowlist validation
      unless ALLOWED_CONFIG_FILES.include?(config_name)
        return render json: { error: 'Config file not allowed' }, status: 403
      end
      
      # Secure path construction
      config_path = Rails.root.join('config', "#{config_name}.yml")
      
      # Ensure path is safe
      unless config_path.to_s.start_with?(Rails.root.join('config').to_s)
        return render json: { error: 'Invalid config path' }, status: 400
      end
      
      unless File.exist?(config_path)
        return render json: { error: 'Config file not found' }, status: 404
      end
      
      # Read and parse safely
      config_content = YAML.safe_load(File.read(config_path))
      
      # Filter sensitive information
      filtered_config = filter_sensitive_config(config_content)
      
      render json: { config: filtered_config }
      
    rescue ArgumentError => e
      render json: { error: e.message }, status: 400
    rescue => e
      Rails.logger.error "Config access error: #{e.message}"
      render json: { error: 'Config access failed' }, status: 500
    end
  end
  
  private
  
  def filter_sensitive_config(config)
    # Remove sensitive keys
    sensitive_keys = %w[password secret_key database_password api_key]
    
    config.reject { |key, _| sensitive_keys.any? { |sk| key.to_s.include?(sk) } }
  end
end

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Rails code opens files with user input: File.open(params[:filename]). User input params[:filename] untrusted. Attackers inject ../ for path traversal. Access /etc/passwd or application secrets. File operations with tainted data enable arbitrary file access.

Root causes

Using User Input Directly in File.open or File.read

Rails code opens files with user input: File.open(params[:filename]). User input params[:filename] untrusted. Attackers inject ../ for path traversal. Access /etc/passwd or application secrets. File operations with tainted data enable arbitrary file access.

send_file with User-Controlled Paths

Controller action uses send_file: send_file(params[:path]). params[:path] tainted with user input. Path traversal to any server file. send_data alternative also vulnerable with user paths. File download features common vulnerability vector.

Not Sanitizing or Validating File Paths

Missing path validation: filepath = Rails.root.join('uploads', params[:file]); File.read(filepath). params[:file] may contain ../../../. join doesn't prevent traversal. Relative paths escape intended directory. Validation required before file operations.

Using Pathname Without Validation

Pathname with user input: Pathname.new(params[:dir]).children. Pathname doesn't validate or restrict paths. User controls directory traversal. Methods like cleanpath insufficient for security. Pathname construction from user input needs validation.

File Operations in Controllers Without Strong Parameters

Accessing params without permit filtering: filename = params[:user][:file]; File.delete(filename). Strong parameters not applied to file operations. Tainted data flows to file methods. Mass assignment protection doesn't prevent file access vulnerabilities.

Fixes

1

Validate Filenames Against Allowlist of Allowed Characters

Strict filename validation: filename = params[:file]; raise unless filename =~ /^[a-zA-Z0-9._-]+$/; File.open(Rails.root.join('uploads', filename)). Allowlist alphanumeric and safe characters. Reject paths with slashes or traversal sequences. Whitelist approach prevents directory traversal.

2

Use realpath or expand_path with Security Checks

Resolve and validate paths: base = Rails.root.join('uploads').realpath; requested = File.expand_path(params[:file], base); raise unless requested.start_with?(base.to_s); File.open(requested). realpath resolves symlinks. Check result within base directory. Prevents traversal outside allowed path.

3

Use Indirect References Instead of Direct File Paths

Map IDs to files: file_id = params[:id]; file_record = UserFile.find_by(id: file_id, user_id: current_user.id); send_file(file_record.path). Database stores actual paths. Users reference by ID. Authorization check included. No direct file path access.

4

Use send_file with Strict Path Validation

Validate before send_file: filename = File.basename(params[:filename]); filepath = Rails.root.join('public', 'downloads', filename).realpath; raise unless filepath.to_s.start_with?(Rails.root.join('public', 'downloads').realpath.to_s); send_file(filepath). basename removes directories. realpath resolves. Boundary check enforces directory restriction.

5

Implement File Access Control with Brakeman Checks

Use Brakeman scanner: brakeman -A to detect tainted file access. Review warnings. Fix identified issues. Brakeman specifically checks for file access with user input. Static analysis catches dangerous patterns. Regular scans prevent introduction.

6

Use Rack::Utils.secure_path for Path Validation

Leverage Rails helpers: include Rack::Utils; secure_path = clean_path_info(params[:path]); validate_path_within_base(secure_path, base_dir). Or custom validation: def safe_path?(path, base); File.expand_path(path, base).start_with?(File.expand_path(base)); end. Use proven validation patterns.

Detect This Vulnerability in Your Code

Sourcery automatically identifies ruby rails tainted file access vulnerability and many other security issues in your codebase.