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.