CrackedRuby logo

CrackedRuby

CGI Cookies

Overview

CGI::Cookie provides Ruby's interface for creating, parsing, and manipulating HTTP cookies in web applications. The class handles cookie serialization, parsing from HTTP headers, and generating proper Set-Cookie header values according to RFC specifications. Ruby implements cookies as objects containing name-value pairs along with attributes like expiration dates, domains, paths, and security flags.

The CGI::Cookie class integrates with Ruby's CGI library to manage stateful information between HTTP requests. When a browser sends cookies through the Cookie header, CGI::Cookie parses the header string into Ruby objects. When sending cookies to browsers, the class generates properly formatted Set-Cookie headers with all necessary attributes and encoding.

require 'cgi'

# Create a new cookie
cookie = CGI::Cookie.new('user_preference', 'dark_theme')
cookie.expires = Time.now + 86400  # 24 hours
cookie.path = '/'
cookie.secure = true

# Generate Set-Cookie header
puts cookie.to_s
# => user_preference=dark_theme; expires=Sun, 31 Aug 2025 12:00:00 GMT; path=/; secure

CGI::Cookie handles multiple values per cookie name, automatic URL encoding of cookie values, and proper handling of special characters. The class maintains separate arrays for cookie names and values, allowing complex data structures while remaining compliant with HTTP cookie specifications.

# Cookie with multiple values
multi_cookie = CGI::Cookie.new('colors', ['red', 'blue', 'green'])
puts multi_cookie.to_s
# => colors=red&blue&green

# Cookie with special characters (automatically encoded)
special_cookie = CGI::Cookie.new('message', 'Hello, World!')
puts special_cookie.to_s
# => message=Hello%2C+World%21

The cookie implementation supports all standard cookie attributes including HttpOnly for XSS protection, SameSite for CSRF protection, and domain restrictions for cross-domain cookie policies. CGI::Cookie automatically handles the complexity of cookie attribute formatting and browser compatibility requirements.

Basic Usage

Creating cookies requires specifying a name and one or more values. The CGI::Cookie constructor accepts various parameter formats, from simple string pairs to complex hash configurations with all cookie attributes.

require 'cgi'

# Simple cookie creation
session_cookie = CGI::Cookie.new('session_id', '3f8a9b2c1d4e5f6')
puts session_cookie.to_s
# => session_id=3f8a9b2c1d4e5f6

# Cookie with expiration
persistent_cookie = CGI::Cookie.new(
  'name' => 'user_token',
  'value' => 'abc123def456',
  'expires' => Time.now + (30 * 86400)  # 30 days
)

Cookie parsing from HTTP headers occurs automatically when processing incoming requests. The CGI library extracts cookie data from the HTTP_COOKIE environment variable and creates CGI::Cookie objects for each cookie sent by the browser.

# Simulate parsing cookies from HTTP header
ENV['HTTP_COOKIE'] = 'user_id=12345; theme=dark; lang=en'
cgi = CGI.new

# Access parsed cookies
user_id = cgi.cookies['user_id']
puts user_id[0]  # => "12345"

theme_cookie = cgi.cookies['theme'] 
puts theme_cookie.value  # => ["dark"]

Setting multiple values in a single cookie allows storing related data together. Each value gets URL-encoded automatically, and the cookie combines values using ampersand separators in the final header output.

# Multiple values in one cookie
preferences = CGI::Cookie.new('prefs', ['notifications_on', 'dark_theme', 'compact_view'])
puts preferences.to_s
# => prefs=notifications_on&dark_theme&compact_view

# Accessing individual values
preferences.value.each { |pref| puts pref }
# => notifications_on
# => dark_theme  
# => compact_view

Cookie attributes control browser behavior and security properties. Path restrictions limit cookie scope to specific URL prefixes, while domain attributes enable cross-subdomain cookie sharing. The secure flag restricts transmission to HTTPS connections only.

# Comprehensive cookie configuration
secure_cookie = CGI::Cookie.new(
  'name' => 'auth_token',
  'value' => 'eyJ0eXAiOiJKV1QiLCJhbGci',
  'path' => '/admin',
  'domain' => '.example.com',
  'expires' => Time.now + 3600,  # 1 hour
  'secure' => true,
  'httponly' => true
)

puts secure_cookie.to_s
# => auth_token=eyJ0eXAiOiJKV1QiLCJhbGci; expires=Sat, 30 Aug 2025 13:00:00 GMT; domain=.example.com; path=/admin; secure; HttpOnly

Error Handling & Debugging

Cookie validation failures occur when cookie names contain invalid characters or when cookie values exceed browser size limitations. CGI::Cookie raises ArgumentError exceptions for malformed cookie names but silently truncates or encodes problematic values.

require 'cgi'

# Invalid cookie names cause exceptions
begin
  invalid_cookie = CGI::Cookie.new('user name', 'john_doe')  # Space in name
rescue ArgumentError => e
  puts "Cookie name error: #{e.message}"
  # Fix with valid name
  valid_cookie = CGI::Cookie.new('user_name', 'john_doe')
end

# Cookie value size validation
large_value = 'x' * 5000  # Exceeds typical 4KB browser limit
size_cookie = CGI::Cookie.new('large_data', large_value)
puts "Cookie size: #{size_cookie.to_s.length} bytes"
# Check if size exceeds browser limits
if size_cookie.to_s.length > 4096
  puts "Warning: Cookie exceeds typical browser limits"
end

Encoding issues arise when cookie values contain non-ASCII characters or reserved URL characters. CGI::Cookie automatically URL-encodes values, but problems occur when manually constructing cookie headers or when receiving malformed cookies from browsers.

# Handling encoding issues
unicode_value = "José's café"  # Contains non-ASCII characters
encoded_cookie = CGI::Cookie.new('username', unicode_value)
puts encoded_cookie.to_s
# => username=Jos%C3%A9%27s+caf%C3%A9

# Debugging encoding problems
def debug_cookie_encoding(cookie)
  original_value = cookie.value.first
  encoded_header = cookie.to_s
  
  puts "Original value: #{original_value}"
  puts "Encoded in header: #{encoded_header}"
  puts "URL decode check: #{CGI.unescape(original_value)}"
end

debug_cookie_encoding(encoded_cookie)

Cookie parsing errors occur when browsers send malformed cookie headers or when cookie values become corrupted. The CGI library attempts to parse invalid cookies but may produce unexpected results requiring validation.

# Simulating malformed cookie parsing
def parse_suspicious_cookies(cookie_header)
  # Manually parse problematic cookie strings
  pairs = cookie_header.split(';').map(&:strip)
  
  pairs.each do |pair|
    if pair.include?('=')
      name, value = pair.split('=', 2)
      begin
        # Validate cookie components
        raise ArgumentError, "Empty cookie name" if name.empty?
        raise ArgumentError, "Cookie name contains spaces" if name.include?(' ')
        
        decoded_value = CGI.unescape(value)
        puts "Valid cookie: #{name} = #{decoded_value}"
      rescue ArgumentError => e
        puts "Invalid cookie ignored: #{pair} (#{e.message})"
      end
    else
      puts "Malformed cookie pair ignored: #{pair}"
    end
  end
end

# Test with problematic cookie data
malformed_header = "valid=value; =empty_name; spaced name=value; valid2=value2"
parse_suspicious_cookies(malformed_header)

Debugging cookie transmission involves examining both outgoing Set-Cookie headers and incoming Cookie headers. Cookie attributes may be ignored or modified by browsers, requiring server-side validation of cookie behavior.

# Cookie debugging utilities
class CookieDebugger
  def self.analyze_cookie(cookie)
    analysis = {
      name: cookie.name,
      values: cookie.value,
      expires: cookie.expires,
      path: cookie.path,
      domain: cookie.domain,
      secure: cookie.secure,
      httponly: cookie.httponly,
      header_length: cookie.to_s.length
    }
    
    # Check for common issues
    warnings = []
    warnings << "Cookie name contains unusual characters" if cookie.name =~ /[^a-zA-Z0-9_-]/
    warnings << "Cookie exceeds 4KB limit" if cookie.to_s.length > 4096
    warnings << "Secure flag without HTTPS context" if cookie.secure && !https_context?
    warnings << "HttpOnly flag may not work in old browsers" if cookie.httponly
    
    analysis[:warnings] = warnings
    analysis
  end
  
  def self.https_context?
    ENV['HTTPS'] == 'on' || ENV['HTTP_X_FORWARDED_PROTO'] == 'https'
  end
end

cookie = CGI::Cookie.new('debug_test', 'test_value', {'secure' => true})
debug_info = CookieDebugger.analyze_cookie(cookie)
puts debug_info

Production Patterns

Web application cookie management requires systematic approaches to cookie lifecycle, security configuration, and performance optimization. Production environments demand consistent cookie policies across application instances and proper handling of cookie-related security headers.

# Production cookie management class
class ProductionCookieManager
  COOKIE_DEFAULTS = {
    secure: true,
    httponly: true,
    samesite: 'Strict',
    path: '/'
  }.freeze
  
  def self.create_session_cookie(session_id, expires_in = 3600)
    CGI::Cookie.new(
      'name' => 'session_id',
      'value' => session_id,
      'expires' => Time.now + expires_in,
      **COOKIE_DEFAULTS
    )
  end
  
  def self.create_csrf_cookie(token)
    CGI::Cookie.new(
      'name' => 'csrf_token', 
      'value' => token,
      'expires' => Time.now + 86400,  # 24 hours
      'secure' => true,
      'samesite' => 'Strict',
      'path' => '/'
    )
  end
  
  def self.create_preferences_cookie(preferences_hash)
    serialized = preferences_hash.to_json
    CGI::Cookie.new(
      'name' => 'user_prefs',
      'value' => Base64.strict_encode64(serialized),
      'expires' => Time.now + (365 * 86400),  # 1 year
      **COOKIE_DEFAULTS.merge(httponly: false)  # Accessible to JavaScript
    )
  end
end

# Usage in web application
session_cookie = ProductionCookieManager.create_session_cookie('abc123')
csrf_cookie = ProductionCookieManager.create_csrf_cookie('def456')

Cookie security in production requires careful attribute configuration and validation of cookie content. Security headers complement cookie attributes to prevent common attacks like XSS and CSRF while maintaining application functionality.

# Security-focused cookie implementation
class SecureCookieHandler
  def self.set_authenticated_user_cookie(cgi, user_id, remember_me = false)
    expires_time = remember_me ? Time.now + (30 * 86400) : Time.now + 3600
    
    # Create signed cookie value to prevent tampering
    signed_value = sign_cookie_value(user_id.to_s)
    
    cookie = CGI::Cookie.new(
      'name' => 'auth_user',
      'value' => signed_value,
      'expires' => expires_time,
      'secure' => true,
      'httponly' => true,
      'samesite' => 'Strict',
      'domain' => ENV['COOKIE_DOMAIN'] || request_domain(cgi),
      'path' => '/'
    )
    
    cgi.out('cookie' => cookie) { "User authenticated" }
  end
  
  def self.verify_cookie_signature(signed_value)
    # Simple HMAC verification (use proper crypto library in production)
    value, signature = signed_value.split('.')
    expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, value)
    
    if signature == expected_signature
      Base64.decode64(value)
    else
      raise SecurityError, "Cookie signature invalid"
    end
  end
  
  private
  
  def self.sign_cookie_value(value)
    encoded_value = Base64.strict_encode64(value)
    signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, encoded_value)
    "#{encoded_value}.#{signature}"
  end
  
  def self.secret_key
    ENV['COOKIE_SECRET'] || raise("COOKIE_SECRET environment variable required")
  end
  
  def self.request_domain(cgi)
    cgi.server_name.sub(/^www\./, '')  # Remove www prefix for broader domain
  end
end

Performance optimization for cookies focuses on minimizing cookie size and reducing redundant cookie transmission. Large cookies increase request overhead, especially for AJAX requests that automatically include all cookies for the domain.

# Cookie performance optimization
class OptimizedCookieStore
  MAX_COOKIE_SIZE = 4000  # Conservative browser limit
  
  def self.store_large_data(name, data_hash)
    serialized = data_hash.to_json
    
    if serialized.length > MAX_COOKIE_SIZE
      # Split large data across multiple cookies
      chunks = serialized.scan(/.{1,#{MAX_COOKIE_SIZE - 50}}/)  # Leave room for metadata
      
      chunks.each_with_index do |chunk, index|
        chunk_cookie = CGI::Cookie.new(
          "#{name}_chunk_#{index}",
          Base64.strict_encode64(chunk),
          {'expires' => Time.now + 86400}
        )
        yield chunk_cookie  # Caller handles cookie output
      end
      
      # Create index cookie to track chunks
      index_cookie = CGI::Cookie.new(
        "#{name}_index",
        chunks.length.to_s,
        {'expires' => Time.now + 86400}
      )
      yield index_cookie
    else
      # Single cookie for smaller data
      single_cookie = CGI::Cookie.new(
        name,
        Base64.strict_encode64(serialized),
        {'expires' => Time.now + 86400}
      )
      yield single_cookie
    end
  end
  
  def self.retrieve_large_data(cgi, name)
    index_cookie = cgi.cookies["#{name}_index"]
    return nil unless index_cookie
    
    chunk_count = index_cookie.value.first.to_i
    chunks = []
    
    (0...chunk_count).each do |i|
      chunk_cookie = cgi.cookies["#{name}_chunk_#{i}"]
      return nil unless chunk_cookie  # Missing chunk
      chunks << Base64.decode64(chunk_cookie.value.first)
    end
    
    JSON.parse(chunks.join)
  rescue JSON::ParserError
    nil  # Invalid data
  end
end

Common Pitfalls

Cookie name restrictions cause subtle failures when applications use invalid characters or reserved words. Browsers silently ignore malformed cookie names, leading to missing cookies that applications expect to receive.

# Problematic cookie names that fail silently
problematic_names = [
  'user name',      # Contains space
  'user-data;',     # Contains semicolon
  'user=info',      # Contains equals sign  
  '',               # Empty name
  'Set-Cookie'      # Reserved header name
]

problematic_names.each do |name|
  begin
    cookie = CGI::Cookie.new(name, 'test_value')
    puts "Created cookie: #{name}"  # May not actually work in browsers
  rescue ArgumentError => e
    puts "Failed to create cookie '#{name}': #{e.message}"
  end
end

# Safe cookie naming patterns
safe_names = ['user_name', 'userData', 'user123', 'session_id']
safe_names.each do |name|
  cookie = CGI::Cookie.new(name, 'test_value')
  puts "Safe cookie created: #{name}"
end

Cookie value encoding issues occur when applications assume values remain unchanged during browser transmission. Special characters, Unicode content, and JSON data require proper encoding to survive the cookie round-trip process.

# Encoding pitfalls demonstration
test_values = [
  'simple_value',           # Works fine
  'value with spaces',      # Gets URL-encoded
  'value;with=special,chars', # Problematic characters
  '{"json": "object"}',     # JSON needs encoding
  'café München',           # Unicode characters
  'value\nwith\nnewlines'   # Control characters
]

test_values.each do |value|
  cookie = CGI::Cookie.new('test', value)
  header_value = cookie.to_s
  
  # Extract value from Set-Cookie header
  extracted = header_value.match(/test=([^;]+)/)[1]
  decoded = CGI.unescape(extracted)
  
  puts "Original: #{value.inspect}"
  puts "In header: #{extracted}"
  puts "Decoded:   #{decoded.inspect}"
  puts "Match: #{value == decoded}"
  puts "---"
end

Domain and path attribute confusion leads to cookies not being sent when expected or being sent to unintended pages. Browser behavior with domain attributes differs from intuitive expectations, especially with subdomain handling.

# Domain attribute pitfalls
def demonstrate_domain_issues
  # These domain settings have unexpected behavior
  examples = [
    {domain: 'example.com', desc: 'Missing dot - only exact domain'},
    {domain: '.example.com', desc: 'With dot - includes subdomains'},  
    {domain: 'www.example.com', desc: 'Subdomain - very restrictive'},
    {domain: '.com', desc: 'TLD domain - browsers reject this'},
    {domain: '', desc: 'Empty domain - uses current domain'}
  ]
  
  examples.each do |config|
    cookie = CGI::Cookie.new(
      'name' => 'domain_test',
      'value' => 'test',
      'domain' => config[:domain]
    )
    
    puts "Domain: '#{config[:domain]}' - #{config[:desc]}"
    puts "Set-Cookie: #{cookie.to_s}"
    puts "Will be sent to:"
    case config[:domain]
    when 'example.com'
      puts "  - example.com only"
    when '.example.com'  
      puts "  - example.com and all subdomains"
    when 'www.example.com'
      puts "  - www.example.com only"
    when '.com'
      puts "  - REJECTED by browsers"
    when ''
      puts "  - Current domain only"
    end
    puts "---"
  end
end

demonstrate_domain_issues

Expiration handling creates bugs when applications assume cookie persistence or immediate deletion. Browser behavior with expired cookies varies, and timezone issues affect expiration calculations in server environments.

# Expiration pitfalls and solutions
class CookieExpirationHandler
  def self.demonstrate_expiration_issues
    # Issue 1: Timezone confusion
    utc_time = Time.utc(2025, 12, 31, 23, 59, 59)
    local_time = Time.local(2025, 12, 31, 23, 59, 59)
    
    utc_cookie = CGI::Cookie.new('utc_test', 'value', {'expires' => utc_time})
    local_cookie = CGI::Cookie.new('local_test', 'value', {'expires' => local_time})
    
    puts "UTC cookie expires: #{utc_cookie.to_s}"
    puts "Local cookie expires: #{local_cookie.to_s}"
    puts "Time difference may cause unexpected behavior"
    
    # Issue 2: Immediate expiration doesn't guarantee deletion
    expired_cookie = CGI::Cookie.new('delete_me', 'value', {'expires' => Time.now - 1})
    puts "Expired cookie: #{expired_cookie.to_s}"
    puts "Browser may still send this cookie briefly"
  end
  
  def self.safe_cookie_deletion(cookie_name)
    # Proper cookie deletion technique
    delete_cookie = CGI::Cookie.new(
      'name' => cookie_name,
      'value' => '',                    # Empty value
      'expires' => Time.at(0),         # Unix epoch time
      'path' => '/',                   # Match original path
      'domain' => '.example.com'       # Match original domain
    )
    delete_cookie
  end
  
  def self.create_session_cookie_safely
    # Session cookies (no expires) vs short-lived cookies
    session_cookie = CGI::Cookie.new('session', 'value')  # No expires = session only
    
    # Short-lived alternative with explicit expiration  
    short_cookie = CGI::Cookie.new(
      'name' => 'temporary',
      'value' => 'value',
      'expires' => Time.now + 300  # 5 minutes from now
    )
    
    [session_cookie, short_cookie]
  end
end

CookieExpirationHandler.demonstrate_expiration_issues

Cookie size limitations cause silent truncation or complete cookie rejection by browsers. Applications storing complex data in cookies must account for size restrictions and implement fallback storage mechanisms.

# Cookie size management
class CookieSizeManager
  BROWSER_LIMITS = {
    chrome: 4096,
    firefox: 4096, 
    safari: 4093,
    edge: 4096,
    conservative: 4000  # Safe limit for all browsers
  }
  
  def self.check_cookie_size(cookie)
    header_string = cookie.to_s
    size = header_string.bytesize
    
    status = {
      size: size,
      safe: size <= BROWSER_LIMITS[:conservative],
      warnings: []
    }
    
    if size > BROWSER_LIMITS[:conservative]
      status[:warnings] << "Exceeds conservative limit (#{BROWSER_LIMITS[:conservative]} bytes)"
    end
    
    if size > BROWSER_LIMITS[:chrome]
      status[:warnings] << "May be rejected by most browsers"
    end
    
    status
  end
  
  def self.compress_cookie_data(data)
    # Demonstrate data compression for large cookies
    json_data = data.to_json
    compressed = Zlib::Deflate.deflate(json_data)
    encoded = Base64.strict_encode64(compressed)
    
    puts "Original JSON: #{json_data.length} bytes"
    puts "Compressed: #{compressed.length} bytes"  
    puts "Base64 encoded: #{encoded.length} bytes"
    
    # Check if compression helps with cookie limits
    test_cookie = CGI::Cookie.new('compressed_data', encoded)
    size_status = check_cookie_size(test_cookie)
    
    puts "Cookie viable: #{size_status[:safe]}"
    size_status[:warnings].each { |warning| puts "Warning: #{warning}" }
    
    encoded
  end
end

# Test with large data
large_data = {
  user_preferences: {
    theme: 'dark',
    language: 'en-US', 
    notifications: ['email', 'push', 'sms'],
    dashboard_layout: ['weather', 'news', 'calendar', 'stocks'] * 20  # Make it large
  }
}

CookieSizeManager.compress_cookie_data(large_data)

Reference

CGI::Cookie Class Methods

Method Parameters Returns Description
new(name, value, **options) name (String), value (String/Array), options (Hash) CGI::Cookie Creates new cookie instance with specified name and values
new(**options) Hash with 'name', 'value', and attribute keys CGI::Cookie Alternative constructor using hash syntax
parse(cookie_string) cookie_string (String) Hash Parses HTTP Cookie header into hash of cookie objects

Instance Methods

Method Parameters Returns Description
name None String Returns cookie name
name=(string) string (String) String Sets cookie name
value None Array Returns array of cookie values
value=(array) array (Array) Array Sets cookie values
domain None String Returns domain attribute
domain=(string) string (String) String Sets domain attribute
path None String Returns path attribute
path=(string) string (String) String Sets path attribute
expires None Time Returns expiration time
expires=(time) time (Time) Time Sets expiration time
secure None Boolean Returns secure flag status
secure=(boolean) boolean (Boolean) Boolean Sets secure flag
httponly None Boolean Returns HttpOnly flag status
httponly=(boolean) boolean (Boolean) Boolean Sets HttpOnly flag
to_s None String Generates Set-Cookie header string

Constructor Options

Option Type Default Description
name String Required Cookie name identifier
value String/Array Required Cookie value(s)
expires Time nil Expiration timestamp
domain String nil Domain restriction
path String nil Path restriction
secure Boolean false HTTPS-only transmission
httponly Boolean false JavaScript access prevention

Cookie Attributes Reference

Attribute Format Effect Browser Support
expires RFC 2822 date Cookie persistence duration Universal
max-age Seconds Alternative to expires Modern browsers
domain Domain string Domain scope control Universal
path Path string URL path restriction Universal
secure Flag only HTTPS transmission only Universal
httponly Flag only Prevents JavaScript access IE6+
samesite Strict/Lax/None CSRF protection Modern browsers

Common Cookie Patterns

# Session cookie (expires when browser closes)
session = CGI::Cookie.new('session_id', 'abc123')

# Persistent cookie with expiration
persistent = CGI::Cookie.new('user_pref', 'value', {
  'expires' => Time.now + 86400
})

# Secure authentication cookie
auth = CGI::Cookie.new('auth_token', 'token', {
  'expires' => Time.now + 3600,
  'secure' => true,
  'httponly' => true,
  'path' => '/admin'
})

# Multi-value cookie
multi = CGI::Cookie.new('selections', ['item1', 'item2', 'item3'])

# Domain-wide cookie
domain_cookie = CGI::Cookie.new('global_pref', 'value', {
  'domain' => '.example.com',
  'path' => '/'
})

Error Types and Handling

Error Cause Solution
ArgumentError Invalid cookie name Use alphanumeric names with underscores/hyphens
ArgumentError Nil cookie name Provide non-empty string for name parameter
Size limit exceeded Cookie header > 4KB Compress data or use server-side storage
Encoding issues Non-ASCII characters Allow automatic URL encoding or pre-encode
Parse failures Malformed cookie headers Validate input before parsing

Browser Compatibility Matrix

Feature Chrome Firefox Safari Edge IE11
Basic cookies
HttpOnly
Secure
SameSite
Max cookie size 4096 4096 4093 4096 4096
Max cookies per domain 180 150 ~600 180 50