Overview
CGI Sessions in Ruby provide server-side session management for web applications through the cgi/session
library. Ruby implements sessions as persistent data stores that maintain user state across HTTP requests using session identifiers transmitted via cookies or URL parameters.
The core session functionality centers around the CGI::Session
class, which handles session creation, data persistence, and cleanup. Sessions store data in key-value pairs and support multiple storage backends including files, memory, and custom stores.
require 'cgi'
require 'cgi/session'
cgi = CGI.new
session = CGI::Session.new(cgi,
'database_manager' => CGI::Session::FileStore,
'session_key' => '_session_id',
'session_expires' => Time.now + 3600
)
session['user_id'] = 12345
session['username'] = 'alice'
session.close
Ruby's session implementation automatically generates unique session identifiers and manages the association between client requests and server-side data. The session ID gets transmitted to the client via cookies or URL parameters, depending on configuration.
# Reading session data
user_id = session['user_id']
if user_id
puts "Welcome back, user #{user_id}"
else
puts "Please log in"
end
Sessions integrate with CGI request processing by examining incoming cookies and URL parameters to locate existing sessions or create new ones. The session manager handles expiration, storage backend selection, and automatic cleanup of expired session data.
Basic Usage
Creating a session requires a CGI object and configuration options specifying the storage backend and session parameters. The most common pattern involves creating the session at the beginning of request processing and closing it when response generation completes.
require 'cgi'
require 'cgi/session'
require 'cgi/session/pstore'
cgi = CGI.new("html4")
session = CGI::Session.new(cgi,
'database_manager' => CGI::Session::PStore,
'session_key' => 'ruby_session_id',
'session_expires' => Time.now + (30 * 60), # 30 minutes
'prefix' => 'rubysess.'
)
The database_manager
parameter determines where session data gets stored. Ruby provides several built-in options: FileStore
for file-based storage, PStore
for marshal-based persistence, and MemoryStore
for in-memory sessions. Each storage backend offers different performance and persistence characteristics.
Sessions behave like hash objects for data access. Store any serializable Ruby object as session values, including strings, numbers, arrays, and custom objects that respond to marshaling.
# Storing various data types
session['user_preferences'] = {
'theme' => 'dark',
'language' => 'en',
'notifications' => true
}
session['shopping_cart'] = ['item1', 'item2', 'item3']
session['last_login'] = Time.now
session['login_attempts'] = 0
Reading session data follows standard hash access patterns. Check for key existence before accessing values to handle cases where session data might not exist or sessions have expired.
# Safe session data access
if session.has_key?('user_id')
user_id = session['user_id']
# Load user data and personalize response
else
# Redirect to login or show anonymous content
end
# Default values for missing session data
theme = session['theme'] || 'light'
cart_items = session['shopping_cart'] || []
Session lifecycle management requires explicit session closure to ensure data persistence. Ruby does not automatically save session changes until session.close
gets called, which writes pending changes to the storage backend.
# Complete request processing cycle
begin
cgi = CGI.new
session = CGI::Session.new(cgi, session_options)
# Process request and modify session data
session['page_views'] = (session['page_views'] || 0) + 1
# Generate response content
puts cgi.header
puts "<html><body>Page views: #{session['page_views']}</body></html>"
ensure
session.close if session
end
Error Handling & Debugging
Session operations can fail due to storage backend issues, permission problems, or corruption in session data. Ruby raises specific exceptions for different session error conditions, requiring proper exception handling around session operations.
Storage backend failures represent the most common source of session errors. File permission issues, disk space problems, or corrupted session files can prevent session creation or data retrieval.
require 'cgi'
require 'cgi/session'
begin
cgi = CGI.new
session = CGI::Session.new(cgi,
'database_manager' => CGI::Session::FileStore,
'session_dir' => '/var/sessions'
)
rescue ArgumentError => e
# Invalid session configuration
puts cgi.header('status' => 'SERVER_ERROR')
puts "Session configuration error: #{e.message}"
rescue Errno::EACCES => e
# Permission denied accessing session directory
puts cgi.header('status' => 'SERVER_ERROR')
puts "Session storage access denied"
rescue StandardError => e
# Other session creation failures
puts cgi.header('status' => 'SERVER_ERROR')
puts "Session initialization failed"
end
Session data corruption can occur when stored objects cannot be unmarshaled or when session files become corrupted. Ruby raises marshal-related exceptions when attempting to load corrupted session data.
# Handling corrupted session data
begin
user_data = session['complex_object']
rescue ArgumentError, TypeError => e
# Marshal errors indicate corrupted session data
Rails.logger.error "Corrupted session data: #{e.message}"
# Clear corrupted session and create fresh one
session.delete
session = CGI::Session.new(cgi, session_options)
# Set flag to prompt user for re-authentication
session['force_login'] = true
end
Session expiration handling requires checking session validity before accessing stored data. Ruby does not automatically clean up expired sessions, so applications must implement expiration logic or use storage backends that support automatic cleanup.
# Session expiration checking
if session['created_at'] &&
Time.now - session['created_at'] > SESSION_TIMEOUT
# Session has expired, clean up and redirect
session.delete
puts cgi.header('status' => 'FOUND', 'location' => '/login')
exit
end
# Update session timestamp
session['last_accessed'] = Time.now
Debugging session issues often requires examining session storage directly and logging session operations. Enable session debugging by inspecting session IDs, storage locations, and data contents.
# Session debugging utilities
def debug_session(session, cgi)
puts "Session ID: #{session.session_id}"
puts "Session new?: #{session.new_session}"
if defined?(session.path) # FileStore backend
puts "Session file: #{session.path}"
puts "File exists: #{File.exist?(session.path)}"
puts "File size: #{File.size(session.path)} bytes" if File.exist?(session.path)
end
puts "Session data keys: #{session.keys.inspect}"
puts "Cookie header: #{cgi.cookies['_session_id']}"
end
Performance & Memory
Session storage backend selection significantly impacts application performance and memory usage. Different backends offer trade-offs between speed, persistence, and scalability that affect overall application performance.
FileStore provides persistent sessions with moderate performance characteristics. File I/O operations occur on every session read and write, creating disk access bottlenecks under high traffic loads.
# FileStore performance characteristics
require 'benchmark'
Benchmark.bm do |x|
x.report("FileStore write:") do
1000.times do |i|
session = CGI::Session.new(cgi,
'database_manager' => CGI::Session::FileStore,
'session_dir' => '/tmp/sessions'
)
session['data'] = "test_data_#{i}"
session.close
end
end
end
# Typical results: 0.8-1.2 seconds for 1000 operations
MemoryStore offers the fastest session operations but lacks persistence across application restarts. Memory usage grows linearly with active session count and stored data size.
# MemoryStore performance comparison
Benchmark.bm do |x|
x.report("MemoryStore write:") do
1000.times do |i|
session = CGI::Session.new(cgi,
'database_manager' => CGI::Session::MemoryStore
)
session['data'] = "test_data_#{i}"
session.close
end
end
end
# Typical results: 0.1-0.3 seconds for 1000 operations
PStore combines file persistence with better performance than FileStore by batching write operations and maintaining in-memory caches. However, PStore files can grow large with frequent updates.
Session data size directly impacts memory usage and serialization performance. Large objects stored in sessions consume memory proportional to the number of active sessions multiplied by average session size.
# Measuring session memory impact
require 'objspace'
# Create session with large data
session['large_array'] = Array.new(10000) { |i| "item_#{i}" }
# Measure object memory usage
session_size = ObjectSpace.memsize_of(session.instance_variable_get(:@data))
puts "Session data memory: #{session_size} bytes"
# Monitor total session memory across requests
total_memory = ObjectSpace.count_objects[:T_OBJECT] *
ObjectSpace.memsize_of({})
puts "Estimated total session memory: #{total_memory} bytes"
Session cleanup impacts both performance and storage usage. Implement regular cleanup processes to remove expired sessions and prevent storage backends from accumulating stale data.
# Automated session cleanup for FileStore
class SessionCleaner
def self.cleanup_expired_sessions(session_dir, max_age = 3600)
expired_count = 0
Dir.glob(File.join(session_dir, 'ruby_sess.*')).each do |session_file|
if File.mtime(session_file) < (Time.now - max_age)
File.unlink(session_file)
expired_count += 1
end
rescue Errno::ENOENT
# File already deleted by another process
end
puts "Cleaned up #{expired_count} expired sessions"
end
end
# Run cleanup periodically
Thread.new do
loop do
SessionCleaner.cleanup_expired_sessions('/var/sessions', 1800)
sleep(300) # Clean up every 5 minutes
end
end
Production Patterns
Production session management requires configuration for security, scalability, and reliability. Sessions in production environments must handle concurrent access, provide adequate security controls, and support horizontal scaling across multiple application servers.
Secure session configuration prevents common session-based attacks through proper settings for session ID generation, cookie attributes, and storage permissions.
# Production-ready session configuration
PRODUCTION_SESSION_CONFIG = {
'database_manager' => CGI::Session::FileStore,
'session_dir' => '/var/app/sessions',
'session_key' => '_secure_session_id',
'session_expires' => Time.now + (2 * 3600), # 2 hours
'new_session' => false,
'prefix' => 'prod_sess_',
'suffix' => '.session'
}.freeze
# Secure cookie configuration
cgi = CGI.new
session = CGI::Session.new(cgi, PRODUCTION_SESSION_CONFIG)
# Set secure cookie attributes
cookie = CGI::Cookie.new(
'name' => '_secure_session_id',
'value' => session.session_id,
'expires' => Time.now + (2 * 3600),
'secure' => true, # HTTPS only
'httponly' => true, # Not accessible via JavaScript
'samesite' => 'Strict' # CSRF protection
)
Load balancing across multiple application servers requires session storage that all servers can access. File-based sessions must use shared storage, or applications should implement database-backed session storage.
# Shared session storage for load-balanced applications
require 'cgi/session/pstore'
class DatabaseSessionStore < CGI::Session::PStore
def initialize(session, option = {})
@database_config = option['database_config'] || {}
super(session, option)
end
private
def path
# Store sessions in database instead of files
session_id = @session.session_id
database_path = "/shared/sessions/#{session_id}.pstore"
# Ensure shared directory exists and has proper permissions
FileUtils.mkdir_p(File.dirname(database_path))
File.chmod(0700, File.dirname(database_path))
database_path
end
end
# Use shared storage in production
session = CGI::Session.new(cgi,
'database_manager' => DatabaseSessionStore,
'database_config' => production_db_config
)
Session monitoring and logging provide visibility into session usage patterns, performance bottlenecks, and security events. Implement comprehensive session logging for production troubleshooting.
# Production session monitoring
class SessionMonitor
def self.log_session_event(event_type, session_id, details = {})
log_entry = {
timestamp: Time.now.iso8601,
event: event_type,
session_id: session_id,
details: details
}
Rails.logger.info("SESSION: #{log_entry.to_json}")
end
def self.track_session_metrics(session)
# Track session usage metrics
session_age = Time.now - (session['created_at'] || Time.now)
data_size = Marshal.dump(session.instance_variable_get(:@data)).size
log_session_event('metrics', session.session_id, {
age_seconds: session_age.to_i,
data_size_bytes: data_size,
keys_count: session.keys.size
})
end
end
# Integrate monitoring into session lifecycle
begin
session = CGI::Session.new(cgi, production_config)
if session.new_session
SessionMonitor.log_session_event('created', session.session_id)
session['created_at'] = Time.now
end
# Track metrics periodically
SessionMonitor.track_session_metrics(session)
ensure
SessionMonitor.log_session_event('closed', session.session_id) if session
session&.close
end
Production deployment requires proper session directory permissions, backup procedures, and recovery plans for session data loss scenarios.
# Production session directory setup
def setup_production_session_storage
session_dir = '/var/app/sessions'
backup_dir = '/var/backups/sessions'
# Create directories with secure permissions
[session_dir, backup_dir].each do |dir|
FileUtils.mkdir_p(dir)
FileUtils.chmod(0700, dir)
FileUtils.chown('app', 'app', dir)
end
# Set up log rotation for session logs
logrotate_config = <<~CONFIG
/var/log/app/sessions.log {
daily
missingok
rotate 30
compress
delaycompress
copytruncate
}
CONFIG
File.write('/etc/logrotate.d/app-sessions', logrotate_config)
end
Common Pitfalls
Session ID prediction represents a critical security vulnerability when session IDs use predictable patterns or insufficient entropy. Ruby's default session ID generation provides adequate randomness, but custom implementations may introduce weaknesses.
# Vulnerable session ID generation (DO NOT USE)
class WeakSessionStore < CGI::Session::FileStore
def create_new_id
# Predictable IDs based on timestamp
"session_#{Time.now.to_i}_#{rand(1000)}"
end
end
# Secure session ID generation
class SecureSessionStore < CGI::Session::FileStore
def create_new_id
# Use cryptographically secure random data
require 'securerandom'
SecureRandom.hex(32) # 256 bits of entropy
end
end
Storing sensitive data directly in sessions creates security risks if session storage becomes compromised or if sessions get transmitted insecurely. Store session references to sensitive data rather than the data itself.
# Insecure: storing sensitive data in session
session['credit_card_number'] = '4111-1111-1111-1111'
session['ssn'] = '123-45-6789'
session['password'] = user_password
# Secure: store references and lookup data server-side
session['user_id'] = user.id
session['payment_token'] = SecureToken.create(user.id)
# Retrieve sensitive data from secure storage when needed
def get_user_payment_info(session)
user_id = session['user_id']
payment_token = session['payment_token']
return nil unless user_id && payment_token
SecureVault.get_payment_data(user_id, payment_token)
end
Race conditions occur when multiple concurrent requests modify the same session simultaneously. Ruby's session implementations are not thread-safe, leading to data corruption or lost updates.
# Race condition example (problematic code)
def increment_counter(session)
current_value = session['counter'] || 0
sleep(0.1) # Simulates processing delay
session['counter'] = current_value + 1
end
# Thread-safe session updates using file locking
require 'fcntl'
def safe_increment_counter(session)
session_file = session.instance_variable_get(:@path)
File.open("#{session_file}.lock", 'w') do |lock_file|
lock_file.flock(File::LOCK_EX)
# Reload session data under lock
current_value = session['counter'] || 0
session['counter'] = current_value + 1
session.close # Force write while locked
end
end
Session fixation attacks occur when applications accept externally provided session IDs without regenerating them after authentication. Always regenerate session IDs during privilege escalation.
# Vulnerable to session fixation
def authenticate_user(cgi, session, username, password)
if valid_credentials?(username, password)
session['authenticated'] = true
session['user_id'] = find_user_id(username)
# Session ID remains the same - vulnerable!
end
end
# Protected against session fixation
def secure_authenticate_user(cgi, session, username, password)
if valid_credentials?(username, password)
# Generate new session ID after authentication
old_data = session.to_hash
session.delete # Remove old session
new_session = CGI::Session.new(cgi, session_options)
new_session['authenticated'] = true
new_session['user_id'] = find_user_id(username)
# Copy over non-sensitive session data
old_data.each do |key, value|
next if ['session_id', 'created_at'].include?(key)
new_session[key] = value
end
new_session
end
end
Memory leaks develop when applications create sessions without proper cleanup or when session storage backends accumulate data without expiration. Monitor memory usage and implement cleanup procedures.
# Memory leak from unclosed sessions
def leaky_session_handling(cgi)
1000.times do
session = CGI::Session.new(cgi, session_options)
session['data'] = Array.new(1000, 'large_string')
# Missing session.close - sessions accumulate in memory
end
end
# Proper session resource management
def proper_session_handling(cgi)
1000.times do
begin
session = CGI::Session.new(cgi, session_options)
session['data'] = process_request_data
# Generate response using session data
generate_response(session)
ensure
session&.close # Always close sessions
end
end
end
Reference
Core Classes and Methods
Class | Purpose | Storage Type |
---|---|---|
CGI::Session |
Main session interface | Backend-dependent |
CGI::Session::FileStore |
File-based session storage | Persistent files |
CGI::Session::PStore |
Marshal-based storage | Persistent binary files |
CGI::Session::MemoryStore |
In-memory session storage | Memory only |
Session Creation Parameters
Parameter | Type | Default | Description |
---|---|---|---|
database_manager |
Class | FileStore |
Storage backend class |
session_key |
String | "_session_id" |
Cookie/parameter name |
session_expires |
Time | nil |
Session expiration time |
new_session |
Boolean | true |
Allow new session creation |
session_dir |
String | "/tmp" |
Directory for file storage |
prefix |
String | "ruby_sess." |
Session file prefix |
suffix |
String | "" |
Session file suffix |
Session Instance Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#[]=(key, value) |
key (String), value (Object) |
Object |
Store session data |
#[](key) |
key (String) |
Object |
Retrieve session data |
#delete(key=nil) |
key (String, optional) |
Object or nil |
Delete session data or entire session |
#has_key?(key) |
key (String) |
Boolean |
Check if session contains key |
#keys |
None | Array<String> |
Get all session data keys |
#close |
None | nil |
Save and close session |
#session_id |
None | String |
Get unique session identifier |
#new_session |
None | Boolean |
Check if session was newly created |
Storage Backend Options
Backend | Persistence | Performance | Scalability | Use Case |
---|---|---|---|---|
FileStore |
Yes | Medium | Limited | Single server |
PStore |
Yes | High | Limited | Single server, complex data |
MemoryStore |
No | Highest | Memory-bound | Development, caching |
Exception Hierarchy
Exception | Cause | Handling Strategy |
---|---|---|
ArgumentError |
Invalid configuration | Validate session parameters |
Errno::EACCES |
Permission denied | Check directory permissions |
Errno::ENOENT |
Missing session file | Create new session |
TypeError |
Marshal errors | Clear corrupted session data |
StandardError |
General session errors | Implement fallback behavior |
Security Configuration Checklist
Setting | Secure Value | Purpose |
---|---|---|
Cookie secure |
true |
HTTPS-only transmission |
Cookie httponly |
true |
Prevent XSS access |
Cookie samesite |
"Strict" |
CSRF protection |
Session timeout | <= 2 hours |
Limit exposure window |
Storage permissions | 0700 |
Restrict file access |
Session ID entropy | >= 128 bits |
Prevent prediction attacks |