# SECURE: Ruby web application without command injection
require 'sinatra'
require 'json'
require 'open3'
require 'digest'
require 'fileutils'
# Configuration
set :show_exceptions, false
# SECURE: File processor with validation
class SecureFileProcessor
ALLOWED_OPERATIONS = %w[info hash lines copy].freeze
SAFE_DIRECTORIES = ['/safe/uploads', '/safe/documents'].freeze
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
def process_file(operation, filename)
raise ArgumentError, 'Operation not allowed' unless ALLOWED_OPERATIONS.include?(operation)
file_path = validate_file_path(filename)
case operation
when 'info'
get_file_info(file_path)
when 'hash'
calculate_hash(file_path)
when 'lines'
count_lines(file_path)
when 'copy'
copy_file(file_path)
end
end
private
def validate_file_path(filename)
# Strict filename validation
raise ArgumentError, 'Invalid filename format' unless filename.match?(/\A[a-zA-Z0-9._-]+\z/)
raise ArgumentError, 'Filename too long' if filename.length > 255
# Find file in safe directories
SAFE_DIRECTORIES.each do |dir|
candidate_path = File.join(dir, filename)
resolved_path = File.expand_path(candidate_path)
# Ensure within safe directory
next unless resolved_path.start_with?(File.expand_path(dir))
if File.exist?(resolved_path) && File.file?(resolved_path)
# Check file size
raise ArgumentError, 'File too large' if File.size(resolved_path) > MAX_FILE_SIZE
return resolved_path
end
end
raise ArgumentError, 'File not found in safe directories'
end
def get_file_info(file_path)
stat = File.stat(file_path)
{
name: File.basename(file_path),
size: stat.size,
modified: stat.mtime.iso8601,
permissions: sprintf('%o', stat.mode)[-3..-1],
readable: File.readable?(file_path)
}
end
def calculate_hash(file_path)
File.open(file_path, 'rb') do |file|
content = file.read
{
file: File.basename(file_path),
size: content.length,
md5: Digest::MD5.hexdigest(content),
sha256: Digest::SHA256.hexdigest(content)
}
end
end
def count_lines(file_path)
lines = File.foreach(file_path).count
{
file: File.basename(file_path),
lines: lines
}
end
def copy_file(file_path)
backup_dir = '/safe/backup'
FileUtils.mkdir_p(backup_dir)
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
backup_name = "#{File.basename(file_path, '.*')}_#{timestamp}#{File.extname(file_path)}"
backup_path = File.join(backup_dir, backup_name)
FileUtils.copy(file_path, backup_path)
{
original: File.basename(file_path),
backup: backup_name,
success: true
}
end
end
# SECURE: Rate limiting
class RateLimiter
def initialize
@requests = Hash.new { |h, k| h[k] = [] }
end
def allowed?(ip, limit = 10, window = 60)
now = Time.now
@requests[ip].reject! { |time| now - time > window }
if @requests[ip].length < limit
@requests[ip] << now
true
else
false
end
end
end
# Initialize components
processor = SecureFileProcessor.new
rate_limiter = RateLimiter.new
# Middleware for rate limiting
before do
client_ip = request.env['HTTP_X_FORWARDED_FOR'] || request.ip
unless rate_limiter.allowed?(client_ip)
halt 429, { error: 'Rate limit exceeded' }.to_json
end
end
# SECURE: File processing endpoint
post '/process' do
content_type :json
begin
data = JSON.parse(request.body.read)
operation = data['operation']
filename = data['filename']
# Validate inputs
halt 400, { error: 'Operation required' }.to_json unless operation
halt 400, { error: 'Filename required' }.to_json unless filename
# Process file securely
result = processor.process_file(operation, filename)
{
status: 'success',
operation: operation,
result: result
}.to_json
rescue JSON::ParserError
halt 400, { error: 'Invalid JSON' }.to_json
rescue ArgumentError => e
halt 400, { error: e.message }.to_json
rescue => e
logger.error "Processing error: #{e.message}"
halt 500, { error: 'Processing failed' }.to_json
end
end
# SECURE: List available operations
get '/operations' do
content_type :json
{
available_operations: SecureFileProcessor::ALLOWED_OPERATIONS,
safe_directories: SecureFileProcessor::SAFE_DIRECTORIES
}.to_json
end
# Error handlers
error 404 do
content_type :json
{ error: 'Endpoint not found' }.to_json
end
error 500 do
content_type :json
{ error: 'Internal server error' }.to_json
end
# Health check
get '/health' do
content_type :json
{ status: 'healthy', timestamp: Time.now.iso8601 }.to_json
end