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