require 'fileutils'
require 'open3'
require 'timeout'
require 'digest'
# SECURE: Rails controller with proper input validation and safe operations
class SecureFileProcessingController < ApplicationController
before_action :validate_and_sanitize_params
before_action :rate_limit_user
before_action :log_security_event
# Security configuration
MAX_FILE_SIZE = 100.megabytes
ALLOWED_EXTENSIONS = %w[.txt .csv .json .xml .log .pdf .jpg .png .gif].freeze
ALLOWED_DIRECTORIES = {
uploads: Rails.root.join('app', 'uploads'),
backups: Rails.root.join('app', 'backups'),
converted: Rails.root.join('app', 'converted'),
archives: Rails.root.join('app', 'archives')
}.freeze
# SECURE: File backup using Ruby FileUtils
def backup_file
filename = params[:filename]
begin
# Comprehensive validation
validate_filename(filename)
source_path = secure_path(:uploads, filename)
backup_filename = generate_backup_name(filename)
backup_path = secure_path(:backups, backup_filename)
# Verify source file exists and is accessible
unless File.exist?(source_path) && File.readable?(source_path)
return render json: { error: 'Source file not found or not accessible' },
status: :not_found
end
# Check file size
if File.size(source_path) > MAX_FILE_SIZE
return render json: { error: 'File too large for backup' },
status: :payload_too_large
end
# Ensure backup directory exists
FileUtils.mkdir_p(File.dirname(backup_path))
# SECURE: Use Ruby's FileUtils instead of system commands
FileUtils.copy_file(source_path, backup_path, preserve: true)
# Verify backup integrity
unless verify_backup_integrity(source_path, backup_path)
File.delete(backup_path) if File.exist?(backup_path)
return render json: { error: 'Backup integrity verification failed' },
status: :internal_server_error
end
Rails.logger.info("Backup successful: #{filename} -> #{backup_filename}")
render json: {
message: 'Backup completed successfully',
backup_filename: backup_filename,
original_size: File.size(source_path),
backup_size: File.size(backup_path)
}
rescue SecurityError => e
Rails.logger.warn("Security violation in backup_file: #{e.message}")
render json: { error: 'Security validation failed' }, status: :forbidden
rescue StandardError => e
Rails.logger.error("Backup error: #{e.message}")
render json: { error: 'Backup operation failed' }, status: :internal_server_error
end
end
# SECURE: File analysis using Ruby's built-in methods
def analyze_file
filename = params[:filename]
begin
validate_filename(filename)
file_path = secure_path(:uploads, filename)
unless File.exist?(file_path)
return render json: { error: 'File not found' }, status: :not_found
end
# SECURE: Use Ruby's File methods instead of system commands
analysis_result = analyze_file_secure(file_path)
render json: {
message: 'File analysis completed',
analysis: analysis_result
}
rescue SecurityError => e
Rails.logger.warn("Security violation in analyze_file: #{e.message}")
render json: { error: 'Security validation failed' }, status: :forbidden
rescue StandardError => e
Rails.logger.error("Analysis error: #{e.message}")
render json: { error: 'File analysis failed' }, status: :internal_server_error
end
end
# SECURE: File compression using Ruby libraries
def compress_files
files = params[:files]
archive_name = params[:archive_name]
begin
# Validate inputs
unless files.is_a?(Array) && files.all? { |f| f.is_a?(String) }
return render json: { error: 'Files must be an array of strings' },
status: :bad_request
end
validate_filename(archive_name)
# Validate each file
files.each { |filename| validate_filename(filename) }
# Check file count limit
if files.length > 50
return render json: { error: 'Too many files (max 50)' },
status: :payload_too_large
end
# SECURE: Use Ruby libraries for compression
archive_path = create_archive_secure(files, archive_name)
render json: {
message: 'Archive created successfully',
archive_name: File.basename(archive_path),
files_count: files.length,
archive_size: File.size(archive_path)
}
rescue SecurityError => e
Rails.logger.warn("Security violation in compress_files: #{e.message}")
render json: { error: 'Security validation failed' }, status: :forbidden
rescue StandardError => e
Rails.logger.error("Compression error: #{e.message}")
render json: { error: 'Archive creation failed' }, status: :internal_server_error
end
end
# SECURE: File search using Ruby's built-in methods
def search_in_file
filename = params[:filename]
pattern = params[:pattern]
begin
validate_filename(filename)
validate_search_pattern(pattern)
file_path = secure_path(:uploads, filename)
unless File.exist?(file_path)
return render json: { error: 'File not found' }, status: :not_found
end
# SECURE: Use Ruby's file reading instead of grep
search_results = search_in_file_secure(file_path, pattern)
render json: {
message: 'Search completed',
results: search_results,
matches_count: search_results.length
}
rescue SecurityError => e
Rails.logger.warn("Security violation in search_in_file: #{e.message}")
render json: { error: 'Security validation failed' }, status: :forbidden
rescue StandardError => e
Rails.logger.error("Search error: #{e.message}")
render json: { error: 'File search failed' }, status: :internal_server_error
end
end
# SECURE: Image conversion using Ruby libraries or safe external calls
def convert_image
input_file = params[:input_file]
output_format = params[:output_format]
quality = params[:quality]&.to_i || 85
begin
validate_image_filename(input_file)
validate_image_format(output_format)
validate_quality(quality)
input_path = secure_path(:uploads, input_file)
unless File.exist?(input_path)
return render json: { error: 'Input file not found' }, status: :not_found
end
# SECURE: Use safe image processing
output_filename = convert_image_secure(input_path, output_format, quality)
render json: {
message: 'Image conversion completed',
output_file: output_filename
}
rescue SecurityError => e
Rails.logger.warn("Security violation in convert_image: #{e.message}")
render json: { error: 'Security validation failed' }, status: :forbidden
rescue StandardError => e
Rails.logger.error("Conversion error: #{e.message}")
render json: { error: 'Image conversion failed' }, status: :internal_server_error
end
end
# SECURE: Network testing using Ruby's built-in networking
def network_test
host = params[:host]
port = params[:port]&.to_i
test_type = params[:test_type]
begin
validate_hostname(host)
validate_port(port) if port
validate_test_type(test_type)
# SECURE: Use Ruby's networking instead of system commands
test_result = perform_network_test_secure(host, port, test_type)
render json: {
message: 'Network test completed',
result: test_result
}
rescue SecurityError => e
Rails.logger.warn("Security violation in network_test: #{e.message}")
render json: { error: 'Security validation failed' }, status: :forbidden
rescue StandardError => e
Rails.logger.error("Network test error: #{e.message}")
render json: { error: 'Network test failed' }, status: :internal_server_error
end
end
private
def validate_and_sanitize_params
# Remove any null bytes or control characters from all string parameters
params.each do |key, value|
if value.is_a?(String)
if value.include?("\0") || value.match?(/[\x00-\x08\x0e-\x1f\x7f]/)
render json: { error: 'Invalid characters in request' }, status: :bad_request
return false
end
end
end
end
def rate_limit_user
# Implement rate limiting (simplified)
session_key = "rate_limit_#{request.remote_ip}"
current_count = Rails.cache.read(session_key) || 0
if current_count >= 60 # 60 requests per hour
render json: { error: 'Rate limit exceeded' }, status: :too_many_requests
return false
end
Rails.cache.write(session_key, current_count + 1, expires_in: 1.hour)
end
def log_security_event
Rails.logger.info("API Request: #{action_name} from #{request.remote_ip} at #{Time.current}")
end
def validate_filename(filename)
raise ArgumentError, 'Filename cannot be blank' if filename.blank?
raise ArgumentError, 'Filename too long' if filename.length > 255
raise SecurityError, 'Invalid filename characters' unless filename.match?(/\A[a-zA-Z0-9._-]+\z/)
raise SecurityError, 'Path traversal detected' if filename.include?('..')
raise SecurityError, 'Hidden files not allowed' if filename.start_with?('.')
extension = File.extname(filename).downcase
raise SecurityError, 'File extension not allowed' unless ALLOWED_EXTENSIONS.include?(extension)
end
def validate_image_filename(filename)
validate_filename(filename)
image_extensions = %w[.jpg .jpeg .png .gif]
extension = File.extname(filename).downcase
raise SecurityError, 'Not an image file' unless image_extensions.include?(extension)
end
def validate_image_format(format)
allowed_formats = %w[jpg jpeg png gif webp]
raise ArgumentError, 'Invalid output format' unless allowed_formats.include?(format.downcase)
end
def validate_quality(quality)
raise ArgumentError, 'Quality must be between 1 and 100' unless (1..100).include?(quality)
end
def validate_search_pattern(pattern)
raise ArgumentError, 'Search pattern cannot be blank' if pattern.blank?
raise ArgumentError, 'Search pattern too long' if pattern.length > 1000
raise SecurityError, 'Invalid pattern characters' unless pattern.match?(/\A[a-zA-Z0-9\s._-]+\z/)
end
def validate_hostname(hostname)
raise ArgumentError, 'Hostname cannot be blank' if hostname.blank?
raise ArgumentError, 'Hostname too long' if hostname.length > 253
hostname_pattern = /\A[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*\z/
raise SecurityError, 'Invalid hostname format' unless hostname.match?(hostname_pattern)
end
def validate_port(port)
raise ArgumentError, 'Invalid port range' unless (1..65535).include?(port)
# Restrict to common service ports for security
allowed_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443, 993, 995]
raise SecurityError, 'Port not in allowed list' unless allowed_ports.include?(port)
end
def validate_test_type(test_type)
allowed_types = %w[ping tcp_connect]
raise ArgumentError, 'Invalid test type' unless allowed_types.include?(test_type)
end
def secure_path(directory_key, filename)
base_dir = ALLOWED_DIRECTORIES[directory_key]
raise ArgumentError, "Unknown directory: #{directory_key}" unless base_dir
path = base_dir.join(filename)
real_path = path.realpath
real_base = base_dir.realpath
unless real_path.to_s.start_with?(real_base.to_s)
raise SecurityError, "Path traversal detected: #{filename}"
end
path
rescue Errno::ENOENT
# For backup paths that don't exist yet, just check the directory
path = base_dir.join(filename)
unless path.to_s.start_with?(base_dir.to_s)
raise SecurityError, "Path traversal detected: #{filename}"
end
path
end
def generate_backup_name(filename)
base = File.basename(filename, File.extname(filename))
ext = File.extname(filename)
timestamp = Time.current.strftime('%Y%m%d_%H%M%S')
"#{base}_backup_#{timestamp}#{ext}"
end
def verify_backup_integrity(source_path, backup_path)
File.size(source_path) == File.size(backup_path) &&
Digest::SHA256.file(source_path).hexdigest == Digest::SHA256.file(backup_path).hexdigest
end
def analyze_file_secure(file_path)
stat = File.stat(file_path)
{
filename: File.basename(file_path),
size: stat.size,
size_human: humanize_bytes(stat.size),
permissions: sprintf('%o', stat.mode)[-4..-1],
created: stat.ctime.iso8601,
modified: stat.mtime.iso8601,
accessed: stat.atime.iso8601,
mime_type: guess_mime_type(file_path),
readable: File.readable?(file_path),
writable: File.writable?(file_path)
}
end
def search_in_file_secure(file_path, pattern)
results = []
line_number = 0
File.foreach(file_path) do |line|
line_number += 1
if line.include?(pattern)
results << {
line_number: line_number,
content: line.strip,
match_position: line.index(pattern)
}
end
# Prevent memory exhaustion
break if results.length >= 1000
end
results
end
def convert_image_secure(input_path, output_format, quality)
output_filename = File.basename(input_path, File.extname(input_path)) + ".#{output_format}"
output_path = secure_path(:converted, output_filename)
# Use MiniMagick (ImageMagick wrapper) for safer image processing
require 'mini_magick'
image = MiniMagick::Image.open(input_path.to_s)
image.format(output_format)
image.quality(quality.to_s)
image.write(output_path.to_s)
output_filename
end
def perform_network_test_secure(host, port, test_type)
case test_type
when 'ping'
test_ping_connectivity(host)
when 'tcp_connect'
test_tcp_connectivity(host, port)
else
raise ArgumentError, "Unsupported test type: #{test_type}"
end
end
def test_ping_connectivity(host)
require 'net/ping'
start_time = Time.current
begin
ping = Net::Ping::External.new(host)
success = ping.ping?
{
success: success,
host: host,
response_time: (Time.current - start_time) * 1000, # ms
message: success ? 'Host is reachable' : 'Host is not reachable'
}
rescue StandardError => e
{
success: false,
host: host,
response_time: 0,
message: "Ping failed: #{e.message}"
}
end
end
def test_tcp_connectivity(host, port)
require 'socket'
require 'timeout'
start_time = Time.current
begin
Timeout::timeout(5) do
TCPSocket.new(host, port).close
end
{
success: true,
host: host,
port: port,
response_time: (Time.current - start_time) * 1000, # ms
message: 'Port is open'
}
rescue Errno::ECONNREFUSED
{
success: false,
host: host,
port: port,
response_time: (Time.current - start_time) * 1000,
message: 'Connection refused'
}
rescue Timeout::Error
{
success: false,
host: host,
port: port,
response_time: 5000,
message: 'Connection timeout'
}
rescue StandardError => e
{
success: false,
host: host,
port: port,
response_time: 0,
message: "Connection failed: #{e.message}"
}
end
end
# Helper methods
def humanize_bytes(bytes)
units = %w[B KB MB GB TB]
size = bytes.to_f
unit_index = 0
while size >= 1024 && unit_index < units.length - 1
size /= 1024
unit_index += 1
end
"%.2f #{units[unit_index]}" % size
end
def guess_mime_type(file_path)
# Simple MIME type guessing
extension = File.extname(file_path).downcase
case extension
when '.txt' then 'text/plain'
when '.json' then 'application/json'
when '.xml' then 'application/xml'
when '.csv' then 'text/csv'
when '.pdf' then 'application/pdf'
when '.jpg', '.jpeg' then 'image/jpeg'
when '.png' then 'image/png'
when '.gif' then 'image/gif'
else 'application/octet-stream'
end
end
end