Like most projects that use Rails 2.1, the Insoshi source code ships with a “secret” string (which lives in environment.rb) needed for the new cookie-based sessions. Recently, an alert observer noted that this raises a security issue in Insoshi sessions: the secret key is currently the same for all Insoshi installations, which opens the sessions up to attack (as noted in this discussion thread). This problem is not unique to Insoshi; it affects essentially any Rails application installed from source.
Part of the reason this problem isn’t more widely known is because projects generated using the rails script automatically receive a unique security string. The way we’ve fixed the secret string problem at Insoshi involves piggybacking on the mechanism Rails already has for generating such strings, by replacing the hard-coded string with a file read:
config/environment.rb
Before:
config.action_controller.session = {
:session_key => '_instant_social_session',
:secret => '63143b62...8522327'
}
After:
.
.
.
require File.join(File.dirname(__FILE__), 'boot')
require 'rails_generator/secret_key_generator'
Rails::Initializer.run do |config|
.
.
.
# Your secret key for verifying cookie session data integrity.
# If you change this key, all old sessions will become invalid!
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
secret_file = File.join(RAILS_ROOT, "secret")
if File.exist?(secret_file)
secret = File.read(secret_file)
else
secret = Rails::SecretKeyGenerator.new("insoshi").generate_secret
File.open(secret_file, 'w') { |f| f.write(secret) }
end
config.action_controller.session = {
:session_key => '_instant_social_session',
:secret => secret
}
.
.
.
(N.B. The session key _instant_social_session is a hint about the origins of the name Insoshi.) In place of a hard-coded string, the updated code uses the contents of a secret file, if it exists; otherwise, it makes a new string using the same machinery as the rails script (included with the line require 'rails_generator/secret_key_generator') and writes it to the secret file.
It’s important at this point to prevent our source code management tool from versioning the secret file, since the whole point of this exercise is to prevent the secret key from being distributed with the source code. Using Git, this is trivial; we just add ’secret’ to our .gitignore file. (Note: if you are running an application on multiple servers, you should copy the same secret file to each one to ensure that sessions will work with a load-balancer.) Everyone using the Insoshi source code should pull from our GitHub repository to get the update.
Handling session expiration
Unfortunately, the above steps don’t completely solve our problem. The comments in environment.rb note that “If you change this key, all old sessions will become invalid!” That’s not quite accurate; the old sessions don’t merely become invalid: they actually raise an exception, so users with active sessions will be met with your application’s error page, and a CGI::Session::CookieStore::TamperedWithCookie exception will show up in your application’s log file. (The error page goes away if the user reloads the page in their browser, but there’s no way for them to know that.) Serving up error pages to all those users isn’t very friendly behavior, and we’d like to catch the exception and show the page they’re trying to access instead.
This isn’t as simple as it seems, because the exception gets raised deep inside the Rails internals. We can figure out where by running in development mode, where the stack trace look something like this:
CGI::Session::CookieStore::TamperedWithCookie in HomeController#index vendor/rails/actionpack/lib/action_controller/session/cookie_store.rb:144:in `unmarshal' vendor/rails/actionpack/lib/action_controller/session/cookie_store.rb:101:in `restore' /usr/local/lib/ruby/1.8/cgi/session.rb:304:in `[]' vendor/rails/actionpack/lib/action_controller/cgi_process.rb:136:in `session' vendor/rails/actionpack/lib/action_controller/cgi_process.rb:168:in `stale_session_check!' vendor/rails/actionpack/lib/action_controller/cgi_process.rb:116:in `session' . . .
To catch the exception, we need to override the default restore method in cookie_store.rb. To do that, we need to load our change before the application loads, and the easiest way to do this is with a plugin, which we can generate with a script:
$ script/generate plugin catch_cookie_exception
Once we edit a couple files, the solution is complete:
vendor/plugins/catch_cookie_exception/init.rb
require 'catch_cookie_exception'
vendor/plugins/catch_cookie_exception/lib/catch_cookie_exception.rb
require 'cgi'
require 'cgi/session'
class CGI::Session::CookieStore
# Restore session data from the cookie.
# This method overrides the one in
# actionpack/lib/action_controller/session/cookie_store.rb
# in order to handle the case of a "tampered" cookie more gracefully.
# The issue is that changing the 'secret' in config/environment.rb
# breaks all sessions in such a way that everyone gets an error page
# the first time they revisit the site. Catching the exception here
# prevents this ugly behavior.
# This is in a plugin so that it loads after Rails but before environment.rb.
def restore
@original = read_cookie
@data = unmarshal(@original) || {}
rescue CGI::Session::CookieStore::TamperedWithCookie
logger = Logger.new("#{RAILS_ROOT}/log/#{RAILS_ENV}.log")
logger.warn "Caught TamperedWithCookie exception on #{Time.now}"
@data = {}
end
end
Note that, since the exception could be the result of someone attacking the site by tampering with their cookies, we log the exception for future reference.
UPDATE: The catch_cookie_exception plugin is now available at GitHub.
Acknowledgments
Thanks again to Trevor Turk for alerting us to this issue.

Rails::SecretKeyGenerator.generate_secret has been deprecated in Rails 2.2. Instead use ActiveSupport::SecureRandom.hex(64)
Comment by Tom — 2009-06-12 @ 06:24
Indeed, you are right, and we updated the Insoshi source code with this fix along with our Rails 2.2 upgrade a couple months ago, but I didn’t update this post. Thanks for helping it stay up-to-date.
Comment by Michael Hartl — 2009-06-12 @ 13:22