By default, rails uses cookie storage for session information. The tutorial I followed said that it was the best way and super fast, and that it all gets encrypted. But when I base64 decode the cookie content, I can see my session info there. It's mixed into a lot of garbled characters, but it's there.
What am I missing here?
Doesn't rails use that secret token thing to encrypt the info in the cookie? How can I make it do so?
Rails uses a secret token to sign the session. The raw data is still there, but changing it will cause it to not match the signature any more, and Rails will reject it. The cookie string looks like session_data--signature, the session data is a base64-encoded marshalled object, and the signature is HMAC(session string, secret token).
The general assumption of the session data is that it is not secret (since it generally should contain only a few things, like a CSRF token and a user ID), but it should not be changeable by a user. The cookie signing accomplishes this.
If you need to actually encrypt the data so that users could never see it, you could do so using something like OpenSSL symmetric encryption, or you could switch to a non-cookie data store.
This is a variant on my own app's cookie store; I haven't tested it, but in theory this should generate actually-encrypted cookies for you. Note that this will be appreciably slower than the default cookie store, and depending on its runtime, could potentially be a DOS vector. Additionally, encrypted data will be lengthier than unencrypted data, and session cookies have a 4kb limit, so if you're storing a lot of data in your session, this might cause you to blow past that limit.
# Define our message encryptor
module ActiveSupport
class EncryptedMessageVerifier < MessageVerifier
def verify(message)
Marshal.load cryptor.decrypt_and_verify(message)
end
def generate(value)
cryptor.encrypt_and_sign Marshal.dump(value)
end
def cryptor
ActiveSupport::MessageEncryptor.new(#secret)
end
end
end
# And then patch it into SignedCookieJar
class ActionDispatch::Cookies::SignedCookieJar
def initialize(parent_jar, secret)
ensure_secret_secure(secret)
#parent_jar = parent_jar
#verifier = ActiveSupport::EncryptedMessageVerifier.new(secret)
end
end
Related
I'm experiencing issues with an elastic load balancer and varnish cache with respect to cookies and sessions getting mixed up between rails and the client. Part of the problem is, rails is adding a "Set-Cookie" header on with a session id on almost every request. If the client already is sending session_id, and it matches the session_id that rails is going to set.. why would rails continuously tell clients "oh yeah.. you're session id is ..."
Summary: Set-Cookie headers are set on almost every response, because
the default session store will try to write the session data to an encrypted cookie on any request that has accessed the session (either to read from it or write to it),
the encrypted value changes even when the plain text value hasn't,
the encryption happens before it reaches the code that's responsible for checking if a cookie value has changed to avoid redundant Set-Cookie headers.
Plain-text cookies
In Rails, the ActionDispatch::Cookies middleware is responsible for writing Set-Cookie response headers based on the contents of a ActionDispatch::Cookies::CookieJar.
The normal behaviour is what you'd expect: if a cookie's value hasn't changed from what was in the request's Cookie header, and the expiry date isn't being updated, then Rails won't send a new Set-Cookie header in the response.
This is taken care of by a conditional in CookieJar#[]= which compares the value already stored in the cookie jar against the new value that's being written.
Encrypted cookies
To handle encrypted cookies, Rails provides an ActionDispatch::Cookies::EncryptedCookieJar class.
The EncryptedCookieJar relies on ActiveSupport::MessageEncryptor to provide the encryption and decryption, which uses a random initialisation vector every time it's called. This means it's almost guaranteed to return a different encrypted string even when it's given the same plain text string. In other words, if I decrypt my session data, and then re-encrypt it, I'll end up with a different string to the one I started with.
The EncryptedCookieJar doesn't do very much: it wraps a regular CookieJar, and just provides encryption as data goes in, and decryption as data comes back out. This means that the CookieJar#[]= method is still responsible for checking if a cookie's value has changed, and it doesn't even know the value it's been given is encrypted.
These two properties of the EncryptedCookieJar explain why setting an encrypted cookie without changing its value will always result in a Set-Cookie header.
The session store
Rails provides different session stores. Most of them store the session data on a server (e.g. in memcached), but the default— ActionDispatch::Session::CookieStore—uses EncryptedCookieJar to store all of the data in an encrypted cookie.
ActionDispatch::Session::CookieStore inherits a #commit_session? method from Rack::Session::Abstract::Persisted, which determines if the cookie should be set. If the session's been loaded, then the answer is pretty much always “yes, set the cookie”.
As we've already seen, in the cases where the session's been loaded but not changed we're still going to end up with a different encrypted value, and therefore a Set-Cookie header.
See the answer by #georgebrock on why this happens. It's pretty easy to patch rails to change this behaviour to only set the cookie if the session changes. Just drop this code in the initializers directory.
require 'rack/session/abstract/id' # defeat autoloading
module ActionDispatch
class Request
class Session # :nodoc:
def changed?;#changed;end
def load_for_write!
load! unless loaded?
#changed = true
end
end
end
end
module Rack
module Session
module Abstract
class Persisted
private
def commit_session?(req, session, options)
if options[:skip]
false
else
has_session = session.changed? || forced_session_update?(session, options)
has_session && security_matches?(req, options)
end
end
end
end
end
end
I'm using SagePay's form integration method with a Ruby on Rails/EmberJS app. I'm handling all the complex payment construction in Rails.
In short, SagePay needs an encrypted, encoded 'crypt' string, which contains data such as the user's billing address, the amount, post-payment redirects, and other transaction data.
SagePay gives an encryption password in the test environment. The form integration guide says to build the crypt as a string, then encrypt it using AES-256 and the encryption password, then Base64 encode the string for POSTing to the Sage test payments server.
Here's how I've implemented this (using the Encryptor gem):
def encryptandencode(string)
salt = Time.now.to_i.to_s
secret_key = 'test-server-secret-key-from-sage'
iv = OpenSSL::Cipher::Cipher.new('aes-256-cbc').random_iv
encrypted_value = Encryptor.encrypt(string, :key => secret_key, :iv => iv, :salt => salt)
encoded = Base64.encode64(encrypted_value).encode('utf-8')
return encoded
end
where string is the unencoded, unencrypted Crypt string containing transaction data.
The problem
Encryptor refuses to use the given secret key. It says the key is too short.
What am I missing here?
I'm struggling to do the same thing in ASP.NET. I don't know why the example 'Integration Kits' they give you on the website are so complicated. They may represent elegant pieces of code in themselves, but they obfuscate how things are working by having functions call functions call methods using settings in the web.config file. For developers new to this API a simple example with all the code in one place would be helpful.
ANYWAY, I still haven't got it working but I have managed to overcome the problem you're having, though my method may not help you since I'm working in ASP.NET. I added a reference to the SagePay.IntegrationKit.DotNet.dll to my project, after which I was able to call the function
SagePay.IntegrationKit.Cryptography.EncryptAndEncode(<name=value collection>, <Encryption Password>)
I now appear to get a valid encrypted string to send to SagePay, my problem is that their website says the encryption is wrong, so this is still a work in progress.
I was struggling with this too, and receiving the same error message.
I finally decided to try each line from the Encryptor gem directly and no longer received that error message. Therefore I have ditched that gem from my Gemfile.
BTW, you have a few things wrong in your example:
you need to use 128 bit encryption, not the default 256: :algorithm => 'aes-128-cbc'
the initialisation vector needs to be the same as the key: :iv => secret_key
you mustn't use a salt
the result needs to be hex encoded not Base64
result = encrypted_value.split('').map { |c| "%02X" % c.ord }.join
The Test and Live Encryption password differ also check your encryption password is 16 characters in length.
Sage Pay Support
I am running Ruby on Rails 3 and I would know if the code that I am using in order to set the cookie value for user authentication purposes is strong enough.
In my model I have:
require 'digest'
class User < ActiveRecord::Base
...
def make_cookie_id_salt(string)
secure_hash("#{self.id}--#{string}")
end
def secure_hash(string)
Digest::SHA2.hexdigest(string)
end
end
In my controller I have:
cookies.signed[:current_user_id] = { :value => [#user.id, #user.make_cookie_id_salt(#user.id)], :expires => 15.days.from_now }
Is it strong enough? If no, how I can improve that (make an example!)?
Everything that gets put into cookies is stored as plain text.
If you set a cookie, and then check the cookies in your browser you will notice (in your case the cookie name would be current_user_id) that it is represented by a string of characters like: G8gcm9sbCB5b3VyIG93biBhdXRoIHRvIGt... (Not quite plain text, right? It is actually Base64 encoded, but you can easily read it - require('base64'); Base64.decode64(string)).
Rails stores a special _yourapp_session cookie that is used to check the cookies validity. If for example someone/something was trying to modify it, it would get rejected.
Now in your case it doesn't really matter if you try to hash something in the cookie or not.
It is just used for authentication (to look up a user in the database by his id) and you are not storing any unique secret data (Which you should not place in a cookie anyway, but it would be the only reason to hash something)
Of course someone could steal the cookie of a user (if he used a public computer and hasn't cleared his cache, etc.) and log in, but there's no way to prevent that (No matter what kind of hashing was used to obfsucate it)
In conclusion you should be fine with what you have.
Rather than try to create your own, I suggest using the Authlogic gem. In a few minutes of configuration you get a complete authentication solution, including cookies and much more. If you really want to roll your own, install the Authlogic gem and take a look at how they do it.
Devise is another option. It's extremely configurable, pretty DRY, with exhausting wiki.
For now-days I prefer it over Authlogic.
Please don't bit my for my misunderstanding.
The sessions are very new for me, and i have some problems.
Okay i read many information about sessions and especially rails session. But this don't give me right imagine about sessions.
Did i understand right, when users send request to server (get) -> Server create a new session (and store this some file in hard drive with session id), session id -> is a random generated num? so, server create a new session (and store session on drive) after this server send back answer to client and set session_id in cookies?
Ok, i debug some params and see some results:
debug(session):
{:_csrf_token=>"jeONIfNxFmnpDn/xt6I0icNK1m3EB3CzT9KMntNk7KU=", :session_id=>"06c5628155efaa6446582c491499af6d", "flash"=>{}}
debug(cookies):
{"remember_user_token"=>"1::3GFRFyXb83lffzwPDPQd", "_blog_session"=>"BAh7CDoQX2NzcmZfdG9rZW4iMWplT05JZk54Rm1ucERuL3h0NkkwaWNOSzFtM0VCM0N6VDlLTW50Tms3S1U9Og9zZXNzaW9uX2lkIiUwNmM1NjI4MTU1ZWZhYTY0NDY1ODJjNDkxNDk5YWY2ZCIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7AA==--348c88b594e98f4bf6389d94383134fbe9b03095"}
Okay, i know, what _csrf_token helps to prevent csrf.
session_id -> is id of the session which stored on hard drive (by default)
but what is _blog_session in cookies?
also, remeber_user_token containes my id (1::*) and what about second part, what is it?
Sorry for this stupid questions, i know what i can easy use any nice auth-plugins (authlogic/clearance/devise), but i want to fully understand sessions.
Thank you.
(also sorry for my english, this is not my native language)
remember_user_token is probably set by your authentication plugin, it is encrypted string, which is stored in users table and is used to authenticate him. Details can vary between plugins.
Second part: you are probably using cookie based session store (it is default),
So, _blog_session stores your encrypted session data.
More about cookie based sessions here and here.
The name "_blog_session" is set in config/initializers/session_store.rb
It looks like:
# 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.
ActionController::Base.session = {
:key => '_blogs_session',
:secret => '07fb6f0d41af4ae06aebb1696fcbb5a5398d4a08570744a4cd53ff237020c43a2022b4041d617d95bcf3f5c4601c7e6c1646eecfc157cc200e7dfedd7d7c6813'
}
I'd like to keep my database clean of stale almost-accounts, and I was thinking about making new signups and invitations put their data into the welcome email as an encrypted or hashed url. Once the link in the url is visited, the information is then added into the database as an account.
Is there something that currently does this? Any references, thoughts, or warnings about doing user registration this way?
Thanks!
Edit:
I've made a working example, and the url is 127 characters.
http://localhost/confirm?_=hBRCGVqie5PetQhjiagq9F6kmi7luVxpcpEYMWaxrtSHIPA3rF0Hufy6EgiH%0A%2BL3t9dcgV9es9Zywkl4F1lcMyA%3D%3D%0A
Obviously, more data = larger url
def create
# Write k keys in params[:user] as v keys in to_encrypt, doing this saves LOTS of unnecessary chars
#to_encrypt = Hash.new
{:firstname => :fn,:lastname => :ln,:email => :el,:username => :un,:password => :pd}.each do |k,v|
#to_encrypt[v] = params[:user][k]
end
encrypted_params = CGI::escape(Base64.encode64(encrypt(compress(Marshal.dump(#to_encrypt)), "secret")))
end
private
def aes(m,t,k)
(aes = OpenSSL::Cipher::Cipher.new('aes-256-cbc').send(m)).key = Digest::SHA256.digest(k)
aes.update(t) << aes.final
end
def encrypt(text, key)
aes(:encrypt, text, key)
end
def decrypt(text, key)
aes(:decrypt, text, key)
end
# All attempts to compress returned a longer url (Bypassed by return)
def compress(string)
return string
z = Zlib::Deflate.new(Zlib::BEST_COMPRESSION)
o = z.deflate(string,Zlib::FINISH)
z.close
o
end
def decompress(string)
return string
z = Zlib::Inflate.new
o = z.inflate(string)
z.finish
z.close
o
end
Thoughts:
Use true asymmetric cypher for the "cookie" to prevent bots creating accounts. Encrypt the "cookie" using public key, verify it by decoding with private key.
Rationale: If only a base64 or other algorithm was used for encoding the cookie, it would be easy to reverse-engineer the scheme and create accounts automatically. This is undesirable because of spambots. Also, if the account is password protected, the password would have to appear in the cookie. Anyone with access to the registration link would be able not only to activate the account, but also to figure out the password.
Require re-entry of the password after activation through the link.
Rationale: Depending on the purpose of the site you may want to improve the protection against information spoofing. Re-entering the password after activation protects against stolen/spoofed activation links.
When verifying the activation link, make sure the account created by it is not created already.
How do you protect against two users simultaneously creating an account with the same name?
Possible answer: Use email as the login identifier and don't require unique account name.
Verify the email first, than continue account creation.
Rationale: This will minimize the information you need to send in the cookie.
There are some e-mail clients which break URLs after 80 letters. I doubt that you can fit all the information in there.
Some browsers have limitations for the URL, Internet Explorer 8 has a limit of 2083 characters, for example.
Why don't you clean your database regularly (cron script) and remove all accounts that haven't been activated for 24 houres?
I have done pretty much the same before. I only have 2 suggestions for you,
Add a key version so you can rotate the key without breaking outstanding confirmation.
You need a timestamp or expiration so you can set a time limit on confirmation if you want to. We allow one week.
As to the shortest URL, you can do better by making following changes,
Use a stream cipher mode like CFB so you don't have to pad to the block size.
Compress the cleartext will help when the data is big. I have a flag and only use compression when it shrinks data.
Use Base64.urlsafe_encode64() so you don't have to URL encode it.
There's a few problems with your solution.
Firstly, you're not setting the IV of the cipher. In my view this has exposed a serious bug in the Ruby OpenSSL wrapper - it shouldn't let you perform an encryption or decryption until both key and iv have been set, but instead it's going ahead and using an IV of all-zeroes. Using the same IV every time basically removes much of the benefit of using a feedback mode in the first place.
Secondly, and more seriously, you have no authenticity checking. One of the properties of CBC mode is that an attacker who has access to one message can modify it to create a second message where a block in the second message has entirely attacker-controlled contents, at the cost of the prior block being completely garbled. (Oh, and note that CFB mode is just as much a problem in this regard).
In this case, that means that I could request an account with Last Name of AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA and my own email address to recieve a valid URL. I can then, without knowing the key, modify the email address to victim#victim.com (and garble the Last Name in the process, which doesn't matter), and have a valid URL which I can submit to your server and create accounts for email addresses that I don't control.
To solve this, you need to compute a HMAC over the data, keyed with a secret that only the server knows, and send that as part of the URL. Note that the only reason you need encryption at all here is to secure the user's password - other than that it could just be plaintext plus a HMAC. I suggest you simply send as the url something like:
?ln=Last%20Name&fn=First%20Name&email=foo#bar.com&hmac=7fpsQba2GMepELxilVUEfwl3%2BN1MdCsg%2FZ59dDd63QE%3D
...and have the verification page prompt for a password (there doesn't seem to be a reason to bounce the password back and forth).
I will take a crack at describing a design that may work.
Prerequisities:
Cryptography library with support for RSA and some secure hash function H (eg. SHA-1)
One pair of private and public keys
Design:
Unique user identifier is e-mail address
An account has associated password and possible other data
The activation cookie is kept as small as possible
Process:
User is asked for e-mail address and password. Upon submission of the form a cookie is computed as
cookie = ENCRYPT(CONCAT(email, '.', H(password)), public key)
E-mail is sent containing a link to the activation page with the cookie, eg.
http://example.org/activation?cookie=[cookie]
The activation page at http://example.org/activation decrypts the cookie passed as parameter: data = SPLIT(DECRYPT(cookie, private key), '.')
In the same activation page the user is asked for password (which must be hashed to the the same value as in cookie) and any other information necessary for the account creation
Upon submission of the activation page a new account is created
Please point out anything that I have missed or any improvements. I'd be glad to update the answer accordingly.