Enter the TamperProofCookieJar
Here’s the code (if it seems familiar it’s because it borrows heavily from the Rails cookie session store):
require 'openssl'
module ActionController
class TamperProofCookieJar < ActionController::CookieJar
class TamperedWithCookie < StandardError;
attr_reader :cookie_name
def initialize(cookie_name)
@cookie_name = cookie_name.to_s
end
end
def initialize(controller, secret, digest = 'SHA1')
super(controller)
@secret = secret
@digest = OpenSSL::Digest::Digest.new(digest)
end
def [](name, protect_from_forgery = false)
if (value = super(name)) && protect_from_forgery
value, hmac = value.to_s.split('--')
unless hmac == generate_digest(value) || hmac == generate_digest(value = CGI.unescape(value))
raise TamperedWithCookie.new(name)
end
end
value
end
def []=(name, options)
if options.is_a?(Hash)
options = options.stringify_keys
options['name'] = name.to_s
options['value'] = options['protect_from_forgery'] == true ? "#{options['value']}--#{generate_digest(options['value'])}" : options['value'].to_s
else
options = { "name" => name.to_s, "value" => options.to_s }
end
set_cookie(options)
end
protected
def generate_digest(value)
OpenSSL::HMAC.hexdigest(@digest, @secret, value.to_s)
end
end
end
The code makes use of the Ruby OpenSSL library to generate a HMAC using the SHA-1 hashing algorithm. A few notes about the code:
- The
TamperedWithCookieexception will be raised on reading a cookie value if it is deemed to have been tampered with. The exception provides acookie_nameattribute that can be used to get the name of the offending cookie. - The
TamperProofCookieJarclass is a subclass of the regular RailsCookieJarand as such is a drop-in replacement for it with one exception: the cookie value is converted to a string before being written to the cookie - this means if you’re not storing a simple string or number then you’ll need to ensure you have marshalled the data correctly before trying to write it to the cookie (again in most situations you shouldn’t be storing any complex objects in a cookie anyway).
Using the TamperProofCookieJar is easy:
# Write a regular cookie (by default HMAC protection is NOT used - so this is just like a regular Rails cookie)
cookies[:test_unprotected] = { :value => user.id, :expires => Time.zone.now + 30.days }
# Write a HMAC protected cookie
# user.id = 100
cookies[:test_protected] = { :value => user.id, :protect_from_forgery => true, :expires => Time.zone.now + 30.days }
# Read a regular cookie (again no protection is the default)
user_id = cookies[:test_unprotected]
=> "100"
# Read a HMAC protected cookie (the boolean parameter turns on HMAC protection)
# Raises TamperProofCookieJar::TamperedWithCookie if the cookie appears to have been changed
user_id = cookies[:test_protected, true]
=> "100"
# Read a raw HMAC protected cookie
# This returns the actual contents of the cookie, including the HMAC, and does not perform tamper checking
user_id = cookies[:test_protected]
=> "100--2157acbbe09b9f412cee3b97ad2e2c2136d3f5e1"
Replacing the Rails CookieJar
If you want to replace the Rails CookieJar with the TamperProofCookieJar in your application you can override the cookies method in your ApplicationController like so:
class ApplicationController < ActionController::Base
def cookies
ActionController::TamperProofCookieJar.new(self, 'this should be a long secret key')
end
end
The second parameter should be a secret key that is hard to guess, just like the one used by the Rails cookie session store.
Performance improvements
As I said earlier, the TamperProofCookieJar was created as part of an optimisation project, so how does its performance compare with the original encryption solution? Using a simple benchmark on 20,000 encryptions versus 20,000 HMACs gives the following results:
- Encryption total time
- 2.918 seconds
- HMAC total time
- 0.854 seconds
This gives a 3.4x performance increase which on a high volume site such as the one I’m working on is a worthy saving!
To patch or plugin?
My original intention was to submit this as a patch to Rails however I don’t think it will be seen as “useful” enough for the wider Rails community to be included in core. Instead I’m leaning towards making it a plugin: so please let me know if you have any problems with the code or have any suggestions for how it can be improved and when time allows I’ll put it all together.
The scenario is straightforward enough: the application stores a numeric record ID in a persistent cookie that, while perfectly safe to be seen by users, shouldn’t be changed by them. The solution currently used in the application is equally straightforward: the data is encrypted (using the 

1 comment
Comment on A tamper-proof cookie jar for Rails by Pedro Sousa
July 31st, 2008 @ 17:47 – permalink
Leave a reply
You can use Markdown in your comment as well as plain HTML. You can use
<filter:jscode lang="ruby">and</filter:jscode>tags to surround code blocks (supported languages are css, html, javascript and ruby). Your email address will not be published.If your comment doesn’t appear immediately after posting it could have been marked as spam. Don’t worry: we regularly check for and approve incorrectly filtered comments so you shouldn’t have to wait too long for it to be shown.