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 |