CrackedRuby CrackedRuby

Overview

Secure file upload handles user-submitted files while preventing malicious uploads from compromising application security, server resources, or user data. File upload functionality creates attack vectors including arbitrary code execution, directory traversal, denial of service, and malware distribution. The upload process requires validation at multiple stages: checking file type, size, content, and destination before accepting any user-provided file.

Applications commonly need file uploads for user avatars, document sharing, media galleries, and data import features. Each upload represents a potential entry point where attackers can inject malicious content disguised as legitimate files. A PHP file uploaded to a web-accessible directory can execute arbitrary code on the server. An SVG file containing JavaScript can execute in a user's browser. An excessively large file can exhaust disk space or memory.

The security model treats all uploaded files as untrusted input. File extensions and MIME types declared by clients are trivial to forge. Content-type headers set by browsers reflect user-controlled data. File content itself requires inspection through multiple validation layers before the application can safely process or store the upload.

# Insecure upload - accepts any file to any location
post '/upload' do
  File.write(params[:file][:filename], params[:file][:tempfile].read)
  "Upload complete"
end

# Secure upload - validates and restricts storage
post '/upload' do
  validator = FileValidator.new(params[:file])
  halt 400, "Invalid file" unless validator.safe?
  
  storage = SecureStorage.new
  storage.save(params[:file])
  "Upload complete"
end

Key Principles

File upload security operates on several foundational principles that address different attack vectors. Defense in depth applies multiple validation layers so that bypassing one control does not compromise the entire system. Input validation examines file characteristics before accepting uploads. Storage isolation separates uploaded content from executable code directories. Access control restricts who can upload, retrieve, and modify files.

Validation Layers

Type validation checks file extensions and MIME types, though both are easily spoofed. Content validation examines the actual file structure and byte patterns. Size validation prevents resource exhaustion attacks. Name validation prevents directory traversal and special character exploits. Each layer catches different classes of attacks.

Storage Security

Uploaded files must never be stored in web-accessible directories where the web server can execute them as code. Directory permissions should prevent uploaded files from being executable. Randomized filenames prevent attackers from predicting file locations or overwriting existing files. Separate storage domains prevent uploaded content from accessing cookies or session data.

Type Detection

File type detection examines magic numbers (byte signatures) at the beginning of files rather than trusting extensions or MIME types. A file claiming to be a JPEG but starting with <?php is executable code, not an image. Libraries like filemagic or system tools like file perform content-based type detection. Image processing libraries can validate that image files contain valid image data.

Content Scanning

Antivirus scanning detects known malware signatures in uploaded files. Document sanitization removes macros, scripts, and active content from office documents. Image reprocessing removes EXIF metadata that might contain malicious payloads. PDF sanitization strips JavaScript and other executable elements.

Access Control

Upload permissions should require authentication and authorization checks. Rate limiting prevents abuse through excessive uploads. Download permissions should verify that users can only access files they uploaded or have been granted access to. Presigned URLs with expiration times control temporary access.

# Content-based type detection
require 'mimemagic'

def detect_file_type(file_path)
  # Don't trust the extension or declared MIME type
  magic = MimeMagic.by_magic(File.open(file_path))
  magic&.type
end

# Validate image content
require 'mini_magick'

def valid_image?(file_path)
  image = MiniMagick::Image.open(file_path)
  image.valid?
  true
rescue MiniMagick::Error
  false
end

Security Implications

File upload vulnerabilities rank among the most critical web application security risks. Understanding attack vectors and implementing appropriate mitigations protects applications from compromise.

Arbitrary Code Execution

The most severe upload vulnerability allows attackers to upload executable code that runs on the server or in other users' browsers. Web shells uploaded as PHP, JSP, or ASP files grant attackers full control over the server. Uploading files to directories where the web server executes scripts creates this vulnerability. Even if uploads are restricted by extension, misconfigurations can allow double extensions like shell.php.jpg or null byte injection like shell.php%00.jpg to bypass filters.

Mitigation requires storing uploads outside the web root or in directories with execution disabled. Web server configuration should explicitly deny execution in upload directories. File extension validation must check all extensions in multi-extension names. Content-type detection must verify actual file content regardless of declared type.

# Prevent execution in upload directory (Apache .htaccess)
# <Directory /var/www/uploads>
#   <FilesMatch "\..*$">
#     SetHandler none
#     ForceType application/octet-stream
#   </FilesMatch>
# </Directory>

# Validate actual content matches expected type
def validate_image_upload(file)
  # Check declared MIME type
  return false unless file.content_type.start_with?('image/')
  
  # Verify with magic numbers
  magic_type = MimeMagic.by_magic(File.open(file.tempfile))
  return false unless magic_type&.type&.start_with?('image/')
  
  # Process with image library to verify structure
  begin
    image = MiniMagick::Image.open(file.tempfile.path)
    return false if image.type.downcase == 'php'
    true
  rescue MiniMagick::Error
    false
  end
end

Path Traversal

Directory traversal attacks use filenames containing ../ sequences to write files outside the intended upload directory. An upload with filename ../../../etc/passwd attempts to overwrite system files. Windows path separators \ and absolute paths starting with / create similar risks. Unicode characters that normalize to path separators bypass naive filters.

Sanitization must remove or reject path components from uploaded filenames. Generating random filenames completely prevents this attack class. If preserving original names is required, strip all directory separators and validate against a whitelist of allowed characters.

# Vulnerable to path traversal
def save_upload(file)
  filename = file.original_filename
  File.write("/var/uploads/#{filename}", file.read)
end

# Secure filename handling
def secure_filename(original_name)
  # Extract extension only
  ext = File.extname(original_name).downcase
  # Generate random name
  "#{SecureRandom.uuid}#{ext}"
end

def save_upload_secure(file)
  filename = secure_filename(file.original_filename)
  path = File.join('/var/uploads', filename)
  
  # Verify resolved path is within upload directory
  real_path = File.realpath(File.dirname(path))
  raise SecurityError unless real_path.start_with?('/var/uploads')
  
  File.write(path, file.read)
end

Denial of Service

Large file uploads exhaust disk space, memory, or bandwidth. Repeated upload attempts consume server resources. Archive bombs (zip bombs) contain highly compressed data that expands to enormous size when extracted. XML bombs use entity expansion to consume processing resources. Image bombs exploit decompression to exhaust memory.

Mitigation sets strict size limits on uploads. Timeouts prevent slow upload attacks. Disk quotas limit storage per user. Asynchronous processing with resource limits handles decompression safely. Rate limiting prevents upload spam.

# Size limit enforcement
MAX_UPLOAD_SIZE = 10.megabytes

def validate_upload_size(file)
  return false if file.size > MAX_UPLOAD_SIZE
  
  # For compressed files, check decompressed size
  if file.content_type == 'application/zip'
    require 'zip'
    total_size = 0
    Zip::File.open(file.tempfile.path) do |zip|
      zip.each do |entry|
        total_size += entry.size
        return false if total_size > MAX_UPLOAD_SIZE * 10
      end
    end
  end
  
  true
end

Content Injection

SVG files can contain embedded JavaScript that executes when viewed in browsers. PDF files can contain JavaScript and forms. HTML files uploaded then served allow XSS attacks. Office documents can contain macros that execute when opened. EXIF metadata in images can contain malicious scripts.

Content sanitization removes dangerous elements from uploaded files. Serving files with strict Content-Security-Policy headers prevents script execution. Setting Content-Disposition: attachment forces downloads rather than inline display. Image reprocessing strips metadata and normalizes formats.

require 'nokogiri'

# Sanitize SVG uploads
def sanitize_svg(svg_path)
  doc = Nokogiri::XML(File.read(svg_path))
  
  # Remove script elements
  doc.xpath('//script').remove
  
  # Remove event handlers
  doc.xpath('//@*[starts-with(name(), "on")]').remove
  
  # Remove javascript: hrefs
  doc.xpath('//a[starts-with(@href, "javascript:")]').remove
  
  File.write(svg_path, doc.to_xml)
end

# Strip EXIF from images
def strip_image_metadata(image_path)
  image = MiniMagick::Image.open(image_path)
  image.strip
  image.write(image_path)
end

Access Control Bypass

Inadequate authorization checks allow users to access files they should not view. Predictable filenames or sequential IDs enable enumeration attacks. Public URLs without authentication expose private files. Missing ownership checks allow users to modify others' files.

Authorization must verify file access on every request. Unpredictable identifiers prevent enumeration. Presigned URLs with expiration provide temporary access without exposing long-term credentials. Audit logs track file access.

# Insecure - predictable IDs
get '/files/:id' do
  file = File.find(params[:id])
  send_file file.path
end

# Secure - verify ownership and use UUIDs
get '/files/:uuid' do
  file = File.find_by!(uuid: params[:uuid])
  
  # Verify user can access this file
  halt 403 unless current_user.can_access?(file)
  
  # Set secure headers
  headers['Content-Disposition'] = 'attachment'
  headers['Content-Security-Policy'] = "default-src 'none'"
  headers['X-Content-Type-Options'] = 'nosniff'
  
  send_file file.storage_path
end

Ruby Implementation

Ruby web frameworks provide file upload handling through request parameters. Secure implementation requires validation, storage management, and access control at the application level.

Rails File Upload Handling

Rails represents uploaded files as ActionDispatch::Http::UploadedFile objects with methods for accessing file data, original names, content types, and temporary storage locations. The params hash contains upload data when a form includes multipart: true.

# Rails controller handling uploads
class AttachmentsController < ApplicationController
  before_action :authenticate_user!
  
  MAX_SIZE = 10.megabytes
  ALLOWED_TYPES = %w[image/jpeg image/png image/gif application/pdf].freeze
  
  def create
    file = params[:attachment]
    
    # Validate presence
    return render json: { error: 'No file provided' }, status: :bad_request unless file
    
    # Validate size
    if file.size > MAX_SIZE
      return render json: { error: 'File too large' }, status: :bad_request
    end
    
    # Validate type
    unless valid_file_type?(file)
      return render json: { error: 'Invalid file type' }, status: :bad_request
    end
    
    # Scan for malware
    if malware_detected?(file.tempfile.path)
      return render json: { error: 'File rejected' }, status: :bad_request
    end
    
    # Create record with secure storage
    attachment = current_user.attachments.create!(
      filename: sanitize_filename(file.original_filename),
      content_type: detect_content_type(file.tempfile.path),
      size: file.size
    )
    
    # Store file
    storage_path = generate_storage_path(attachment)
    FileUtils.mkdir_p(File.dirname(storage_path))
    FileUtils.mv(file.tempfile.path, storage_path)
    
    attachment.update!(storage_path: storage_path)
    
    render json: attachment, status: :created
  end
  
  private
  
  def valid_file_type?(file)
    # Check declared type
    return false unless ALLOWED_TYPES.include?(file.content_type)
    
    # Verify actual content
    magic = MimeMagic.by_magic(File.open(file.tempfile.path))
    ALLOWED_TYPES.include?(magic&.type)
  end
  
  def sanitize_filename(original)
    # Remove path components
    name = File.basename(original)
    # Remove dangerous characters
    name.gsub(/[^a-zA-Z0-9\-_.]/, '_')
  end
  
  def generate_storage_path(attachment)
    # Store outside webroot with random subdirectories
    uuid = attachment.uuid
    File.join(
      Rails.root, 'storage', 'uploads',
      uuid[0..1], uuid[2..3], uuid
    )
  end
  
  def detect_content_type(path)
    magic = MimeMagic.by_magic(File.open(path))
    magic&.type || 'application/octet-stream'
  end
  
  def malware_detected?(path)
    # Integrate with ClamAV or similar
    result = `clamscan --no-summary #{path} 2>&1`
    result.include?('FOUND')
  end
end

Active Storage Integration

Rails Active Storage provides file upload management with validation, storage backends, and attachment associations. The gem handles storage abstraction but requires security configuration.

# Model with Active Storage attachment
class User < ApplicationRecord
  has_one_attached :avatar
  
  validate :acceptable_avatar
  
  def acceptable_avatar
    return unless avatar.attached?
    
    # Size validation
    if avatar.byte_size > 5.megabytes
      errors.add(:avatar, 'is too large')
    end
    
    # Type validation
    acceptable_types = %w[image/jpeg image/png]
    unless acceptable_types.include?(avatar.content_type)
      errors.add(:avatar, 'must be a JPEG or PNG')
    end
    
    # Content validation for images
    if avatar.content_type.start_with?('image/')
      begin
        image = MiniMagick::Image.read(avatar.download)
        errors.add(:avatar, 'is corrupted') unless image.valid?
      rescue MiniMagick::Error
        errors.add(:avatar, 'is not a valid image')
      end
    end
  end
end

# Controller with Active Storage
class AvatarsController < ApplicationController
  def update
    if current_user.update(avatar_params)
      # Process image: resize and strip metadata
      current_user.avatar.variant(resize_to_limit: [500, 500]).processed
      
      redirect_to profile_path
    else
      render :edit, status: :unprocessable_entity
    end
  end
  
  private
  
  def avatar_params
    params.require(:user).permit(:avatar)
  end
end

# Configure Active Storage service (config/storage.yml)
# local:
#   service: Disk
#   root: <%= Rails.root.join("storage") %>
#
# amazon:
#   service: S3
#   access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
#   secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
#   region: us-east-1
#   bucket: secure-uploads

Sinatra File Upload

Sinatra provides basic file upload handling. Security implementation requires manual validation and storage management.

require 'sinatra'
require 'securerandom'
require 'mimemagic'

configure do
  set :upload_path, File.join(Dir.pwd, 'uploads')
  set :max_upload_size, 10 * 1024 * 1024
end

helpers do
  def secure_upload(file_param)
    file = params[file_param]
    halt 400, 'No file provided' unless file
    
    # Validate size
    halt 400, 'File too large' if file[:tempfile].size > settings.max_upload_size
    
    # Detect type
    magic = MimeMagic.by_magic(file[:tempfile])
    halt 400, 'Invalid file type' unless magic && magic.image?
    
    # Generate secure filename
    ext = File.extname(file[:filename]).downcase
    secure_name = "#{SecureRandom.uuid}#{ext}"
    
    # Save file
    save_path = File.join(settings.upload_path, secure_name)
    File.open(save_path, 'wb') do |f|
      file[:tempfile].rewind
      f.write(file[:tempfile].read)
    end
    
    secure_name
  end
end

post '/upload' do
  content_type :json
  
  begin
    filename = secure_upload(:file)
    { success: true, filename: filename }.to_json
  rescue => e
    halt 500, { error: e.message }.to_json
  end
end

get '/files/:filename' do
  # Validate filename format
  halt 400 unless params[:filename] =~ /\A[a-f0-9\-]{36}\.[a-z]+\z/
  
  file_path = File.join(settings.upload_path, params[:filename])
  halt 404 unless File.exist?(file_path)
  
  send_file file_path, disposition: 'attachment'
end

Rack Middleware for Upload Security

Custom Rack middleware can enforce upload policies across the application stack.

class SecureUploadMiddleware
  def initialize(app, options = {})
    @app = app
    @max_size = options[:max_size] || 10.megabytes
    @allowed_types = options[:allowed_types] || []
  end
  
  def call(env)
    request = Rack::Request.new(env)
    
    if upload_request?(request)
      validate_uploads!(request)
    end
    
    @app.call(env)
  rescue SecurityError => e
    [400, { 'Content-Type' => 'application/json' }, 
     [{ error: e.message }.to_json]]
  end
  
  private
  
  def upload_request?(request)
    request.post? && request.content_type&.start_with?('multipart/form-data')
  end
  
  def validate_uploads!(request)
    request.params.each do |key, value|
      next unless value.is_a?(Hash) && value[:tempfile]
      
      validate_size!(value)
      validate_type!(value)
    end
  end
  
  def validate_size!(upload)
    if upload[:tempfile].size > @max_size
      raise SecurityError, "Upload exceeds maximum size"
    end
  end
  
  def validate_type!(upload)
    return if @allowed_types.empty?
    
    magic = MimeMagic.by_magic(File.open(upload[:tempfile].path))
    unless magic && @allowed_types.include?(magic.type)
      raise SecurityError, "File type not allowed"
    end
  end
end

# Use middleware in config.ru
use SecureUploadMiddleware, 
    max_size: 10.megabytes,
    allowed_types: %w[image/jpeg image/png application/pdf]

Common Pitfalls

File upload implementations frequently contain security vulnerabilities due to common mistakes and misconceptions.

Trusting Client-Provided Data

File extensions, MIME types, and content-type headers all originate from the client and can be trivially forged. An attacker sets any extension or MIME type when uploading a file. Relying solely on these indicators allows malicious file types to bypass validation.

# Vulnerable - trusts client-provided MIME type
def validate_upload(file)
  file.content_type == 'image/jpeg'
end

# Secure - verifies actual content
def validate_upload(file)
  magic = MimeMagic.by_magic(File.open(file.tempfile.path))
  magic&.type == 'image/jpeg'
end

Insufficient Extension Validation

Checking only the final extension misses double extensions like shell.php.jpg. Case-insensitive checks miss uppercase extensions. Blacklist approaches miss novel extensions. File systems may interpret extensions differently than the validation logic.

# Vulnerable - checks only last extension
def safe_extension?(filename)
  %w[.jpg .png .gif].include?(File.extname(filename).downcase)
end

# Vulnerable - allows double extensions
filename = "shell.php.jpg"
File.extname(filename) # => ".jpg" (appears safe)

# Secure - verify no dangerous extensions anywhere
def safe_filename?(filename)
  dangerous = %w[.php .jsp .asp .cgi .pl .py .rb .sh .exe .bat]
  parts = filename.downcase.split('.')
  (dangerous & parts.map { |p| ".#{p}" }).empty?
end

Preserving Original Filenames

Using client-provided filenames creates directory traversal vulnerabilities and filename collisions. Attackers craft filenames with path separators, null bytes, or special characters that exploit filesystem handling.

# Vulnerable - uses original filename
def save_file(upload)
  File.write("uploads/#{upload.original_filename}", upload.read)
end

# Attacker uploads with filename: "../../../etc/cron.d/evil"

# Secure - generate random filenames
def save_file(upload)
  ext = File.extname(upload.original_filename)
  filename = "#{SecureRandom.uuid}#{ext}"
  File.write("uploads/#{filename}", upload.read)
end

Storing in Web-Accessible Directories

Placing uploads in public web directories allows direct access without going through application authorization logic. Uploaded executable files run on the server. HTML or SVG files execute scripts in user browsers.

# Vulnerable - stores in public directory
def save_upload(file)
  path = File.join(Rails.root, 'public', 'uploads', file.original_filename)
  File.write(path, file.read)
  # File accessible at /uploads/filename
end

# Secure - stores outside webroot
def save_upload(file)
  filename = SecureRandom.uuid
  path = File.join(Rails.root, 'storage', 'uploads', filename)
  FileUtils.mkdir_p(File.dirname(path))
  File.write(path, file.read)
  # Requires controller action with authorization
end

Missing Size Limits

Unlimited upload sizes enable denial-of-service through disk exhaustion or memory consumption. Processing large files without streaming loads entire contents into memory. Decompressing archives without size checks triggers zip bombs.

# Vulnerable - no size limits
post '/upload' do
  File.write(params[:file][:filename], params[:file][:tempfile].read)
end

# Secure - enforces limits at multiple layers
post '/upload' do
  file = params[:file]
  
  # Check uploaded size
  halt 400 if file[:tempfile].size > 10.megabytes
  
  # Stream large files to avoid memory issues
  destination = "uploads/#{SecureRandom.uuid}"
  File.open(destination, 'wb') do |f|
    file[:tempfile].rewind
    IO.copy_stream(file[:tempfile], f)
  end
end

Inadequate Type Validation

MIME type sniffing by browsers can cause files to execute as types different from their declared content-type. Images containing embedded JavaScript execute when served as HTML. Polyglot files valid as multiple formats bypass type checks.

# Vulnerable - serves uploads with detected type
get '/files/:id' do
  file = File.find(params[:id])
  send_file file.path, type: file.content_type
end

# Browser may ignore content-type and sniff actual type

# Secure - force download and prevent sniffing
get '/files/:id' do
  file = File.find(params[:id])
  headers['X-Content-Type-Options'] = 'nosniff'
  headers['Content-Disposition'] = 'attachment'
  headers['Content-Security-Policy'] = "default-src 'none'"
  send_file file.storage_path, type: 'application/octet-stream'
end

Testing Approaches

Testing secure file upload requires verifying both positive cases (valid uploads succeed) and negative cases (invalid uploads are rejected). Security tests confirm that various attack vectors fail appropriately.

Unit Testing Validation Logic

Validation methods require tests covering all acceptance criteria and edge cases. Tests verify size limits, type restrictions, content validation, and filename sanitization.

# RSpec tests for upload validator
RSpec.describe FileValidator do
  describe '#valid_type?' do
    it 'accepts valid JPEG files' do
      file = fixture_file_upload('valid.jpg', 'image/jpeg')
      validator = FileValidator.new(file)
      expect(validator.valid_type?).to be true
    end
    
    it 'rejects PHP files with JPEG extension' do
      file = fixture_file_upload('shell.php.jpg', 'image/jpeg')
      validator = FileValidator.new(file)
      expect(validator.valid_type?).to be false
    end
    
    it 'rejects files based on magic numbers' do
      # Create file with JPEG extension but PHP content
      tempfile = Tempfile.new(['test', '.jpg'])
      tempfile.write('<?php system($_GET["cmd"]); ?>')
      tempfile.rewind
      
      file = double('upload', tempfile: tempfile, content_type: 'image/jpeg')
      validator = FileValidator.new(file)
      expect(validator.valid_type?).to be false
    end
  end
  
  describe '#safe_filename?' do
    it 'accepts simple alphanumeric names' do
      validator = FileValidator.new(double(original_filename: 'document.pdf'))
      expect(validator.safe_filename?).to be true
    end
    
    it 'rejects path traversal attempts' do
      validator = FileValidator.new(double(original_filename: '../../../etc/passwd'))
      expect(validator.safe_filename?).to be false
    end
    
    it 'rejects null byte injection' do
      validator = FileValidator.new(double(original_filename: "shell.php\x00.jpg"))
      expect(validator.safe_filename?).to be false
    end
  end
end

Integration Testing Upload Endpoints

Controller tests verify the complete upload flow including authentication, validation, storage, and response handling.

# RSpec controller tests
RSpec.describe AttachmentsController do
  describe 'POST #create' do
    context 'with valid file' do
      let(:file) { fixture_file_upload('document.pdf', 'application/pdf') }
      
      it 'creates attachment record' do
        expect {
          post :create, params: { attachment: file }
        }.to change(Attachment, :count).by(1)
      end
      
      it 'stores file securely' do
        post :create, params: { attachment: file }
        attachment = Attachment.last
        
        # Verify file stored outside webroot
        expect(attachment.storage_path).not_to include('public')
        
        # Verify file exists
        expect(File.exist?(attachment.storage_path)).to be true
      end
      
      it 'generates unpredictable filename' do
        post :create, params: { attachment: file }
        attachment = Attachment.last
        
        # Verify UUID format
        expect(attachment.storage_path).to match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)
      end
    end
    
    context 'with malicious file' do
      it 'rejects PHP file with image extension' do
        file = fixture_file_upload('shell.php.jpg', 'image/jpeg')
        post :create, params: { attachment: file }
        
        expect(response).to have_http_status(:bad_request)
        expect(Attachment.count).to eq(0)
      end
      
      it 'rejects oversized files' do
        large_file = double('upload', 
                           size: 100.megabytes,
                           original_filename: 'large.pdf',
                           content_type: 'application/pdf')
        allow(params).to receive(:[]).with(:attachment).and_return(large_file)
        
        post :create, params: { attachment: large_file }
        expect(response).to have_http_status(:bad_request)
      end
    end
    
    context 'authorization' do
      it 'requires authentication' do
        post :create, params: { attachment: file }
        expect(response).to redirect_to(login_path)
      end
      
      it 'enforces upload limits per user' do
        user.attachments.create!(Array.new(100) { build(:attachment) })
        
        post :create, params: { attachment: file }
        expect(response).to have_http_status(:forbidden)
      end
    end
  end
end

Security Testing Attack Vectors

Security tests explicitly attempt common attacks to verify they fail safely.

RSpec.describe 'Upload Security' do
  describe 'path traversal protection' do
    it 'prevents directory traversal in filename' do
      file = fixture_file_upload('test.jpg', 'image/jpeg')
      allow(file).to receive(:original_filename).and_return('../../../etc/passwd')
      
      post '/upload', attachment: file
      
      expect(response).to have_http_status(:bad_request)
      expect(File.exist?('/etc/passwd')).to be true # Original not overwritten
    end
  end
  
  describe 'type confusion protection' do
    it 'detects polyglot files' do
      # Create file valid as both GIF and PHP
      polyglot = Tempfile.new(['polyglot', '.gif'])
      polyglot.write("GIF89a<?php system(\$_GET['c']); ?>")
      polyglot.rewind
      
      file = double('upload', 
                   tempfile: polyglot,
                   content_type: 'image/gif',
                   original_filename: 'image.gif')
      
      post '/upload', attachment: file
      expect(response).to have_http_status(:bad_request)
    end
  end
  
  describe 'content validation' do
    it 'rejects SVG with embedded JavaScript' do
      svg = '<svg><script>alert("XSS")</script></svg>'
      file = create_upload_file(svg, 'image.svg', 'image/svg+xml')
      
      post '/upload', attachment: file
      
      if response.status == 200
        # If accepted, verify script was stripped
        stored_content = File.read(Attachment.last.storage_path)
        expect(stored_content).not_to include('<script>')
      end
    end
  end
end

Reference

Upload Validation Checklist

Validation Type Check Implementation
Size File does not exceed maximum size Compare tempfile.size against limit before processing
Extension Filename extension in whitelist Extract all extensions, verify against allowed list
MIME Type Content-Type header in allowed types Check file.content_type against whitelist
Magic Numbers File signature matches expected type Use MimeMagic or system file command on content
Content Structure File content is valid for declared type Process with type-specific library (MiniMagick, Nokogiri)
Filename No path components or special characters Sanitize or replace with generated identifier
Malware No malicious signatures detected Scan with antivirus tool before storage
Rate Limit User has not exceeded upload quota Check upload count and total size per time period

Security Headers for File Downloads

Header Value Purpose
Content-Disposition attachment; filename="file.ext" Force download instead of inline display
X-Content-Type-Options nosniff Prevent MIME type sniffing
Content-Security-Policy default-src 'none' Block script execution in served files
X-Frame-Options DENY Prevent embedding in frames
Content-Type application/octet-stream Serve as binary data regardless of actual type

Rails Active Storage Validators

Validator Usage Example
attached Verify file attached validates :avatar, attached: true
content_type Restrict MIME types validates :document, content_type: ['application/pdf']
size Limit file size validates :image, size: { less_than: 5.megabytes }
dimension Restrict image dimensions validates :avatar, dimension: { width: { max: 2000 }, height: { max: 2000 } }

Storage Path Generation Patterns

Pattern Security Benefit Example Path
UUID filename Unpredictable, no collisions storage/a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf
Hash-based subdirectories Distributed storage, no enumeration storage/a1/b2/a1b2c3d4e5f6.pdf
User-specific paths Authorization at filesystem level storage/users/12345/documents/uuid.pdf
Date-based organization Efficient cleanup, audit trails storage/2025/10/10/uuid.pdf

Content-Type Detection Methods

Method Reliability Speed Usage
File extension Low (trivial to forge) Instant Initial filter only, never final decision
HTTP Content-Type header Low (client-controlled) Instant Cross-check with other methods
Magic number inspection High (checks actual content) Fast Primary validation method
Library parsing Very high (validates structure) Moderate Confirmation for critical file types
Antivirus scanning High (detects known malware) Slow Optional additional layer

Ruby File Handling Methods

Method Purpose Security Note
File.basename Extract filename without path Removes directory traversal but preserves extension
File.extname Get file extension Returns only final extension, check full name for doubles
File.realpath Resolve symbolic links and relative paths Use to verify final path within allowed directory
FileUtils.mkdir_p Create directory structure safely Recursive directory creation without race conditions
IO.copy_stream Stream large files efficiently Avoids loading entire file into memory
Tempfile.new Create secure temporary file Automatic cleanup and unique naming