Detecting Rails 4 Session cookie tampering - ruby-on-rails

Background
I'm an experienced web developer (mostly with Python and CherryPy) who has implemented secure session management from scratch before, and is now learning Rails. I'm investigating the behavior of Rails sessions as exposed by the session object that is available in the ActionController instance and view contexts.
Question/Problem
I have read that the default implementation of sessions in Rails 4 uses an encrypted and tamper-proof cookie. Cool, I guess that means I can use it to hold a user ID for user sessions without worrying about session forging (tamper-proof) or anyone being able to find out what their ID is (encrypted). I wanted to test this and see what rails would do if the session cookie was altered.
So, I went and altered the content of the session cookie attribute using a browser add-on, and when I reload the page with the new cookie value, Rails just happily gives me different new values for session_id and _csrf_token.
What happened to session cookie integrity!?
Shouldn't rails detect (via HMAC signature) that the cookie was altered and then tell me about it somehow?
I'm terrified that I'm missing something obscenely obvious, but I've been having no luck searching for an answer on the web, and the source code isn't giving it up easily either (I'm new to ruby). Thanks in advance.
The Experiment
I created a new app and generated a controller with an index action:
$ rails new my_app
$ cd my_app; rails g controller home index
Then I added these two lines to the app/views/layouts/application.html.erb file:
<%= session.keys %><br/>
<%= session.values %>
I started up the dev server and navigated my browser to "localhost:3000/home/index". As expected, the page has the following lines at the bottom:
["session_id", "_csrf_token"]
["8c1558cabe6c86cfb37d6191f2e03bf8", "S8i8/++8t6v8W8RMeyvnNu3Pjvj+KkMo2UEcm1oVVZg="]
Reloading the page gives me the same values, although the app sets a new value of the _my_app_session cookie attribute every time. That seems weird to me, but I'm getting the same session hash values, so I guess it's cool.
Then, I used a cookie editing add-on for Chrome to alter the value of the _my_app_session cookie attribute (replacing the first character of the attribute value). Reloading the page shows completely different values without anything happening. WAT?

I can't claim a really thorough understanding of the code here. But I can tell you this much:
I followed your steps exactly (using Ruby 2.0.0-p247 & Rails 4.0), with one exception -- I also added the 'byebug' gem to my Gemfile and inserted a debugging breakpoint in the HomeController#index action.
From the byebug console, at that breakpoint, I could see the unedited session cookie via:
(byebug) cookies["_my_app_session"]
"cmtWeEc3VG5hZ1BzUzRadW5ETTRSaytIQldiaTMyM0NtTU14c2RrcVVueWRQbncxTnJzVDk3OWU3N21PWWNzb1IrZDUxckdMNmZ0cGl3Mk0wUGUxU1ZWN3BmekFVQTFxNk55OTRwZStJSmtJZVkzVmlVaUI2c2c5cDRDWVVMZ0lJcENmWStESjhzRU81MHFhRTN4VlNWRlJKYTU3aFVLUDR5Y1lSVkplS0J1Wko3R2IxdkVYS3IxTHA2eC9kOW56LS1IbXlmelRlSWxiaG02Q3N2L0tUWHN3PT0=--b37c705a525ab2fb14feb5f2edf86d3ae1ab03c5"
And I could see the actual encrypted values with
(byebug) cookies.encrypted["_my_app_session"]
{"session_id"=>"13a95fb545a1e3a2d4e9b4c22debc260", "_csrf_token"=>"FXb8pZgmoK0ui0qCW8W75t3sN2KLRpkiFBmLbHSfnhc="}
Now, I edit the cookie by changing the first letter to "A" and refresh the page:
(byebug) cookies["_my_app_session"]
"AmtWeEc3VG5hZ1BzUzRadW5ETTRSaytIQldiaTMyM0NtTU14c2RrcVVueWRQbncxTnJzVDk3OWU3N21PWWNzb1IrZDUxckdMNmZ0cGl3Mk0wUGUxU1ZWN3BmekFVQTFxNk55OTRwZStJSmtJZVkzVmlVaUI2c2c5cDRDWVVMZ0lJcENmWStESjhzRU81MHFhRTN4VlNWRlJKYTU3aFVLUDR5Y1lSVkplS0J1Wko3R2IxdkVYS3IxTHA2eC9kOW56LS1IbXlmelRlSWxiaG02Q3N2L0tUWHN3PT0=--b37c705a525ab2fb14feb5f2edf86d3ae1ab03c5"
(byebug) cookies.encrypted["_my_app_session"]
nil
So the session is nil at this point in the request:
(byebug) session
#<ActionDispatch::Request::Session:0x7ff41ace4bc0 not yet loaded>
I can force loading the session with
(byebug) session.send(:load!)
and when I do, I see that the resulting session id is
"f6be13fd646962de676985ec9bb4a8d3"
and sure enough, when I let the request finish, that's what I see in the view:
["session_id", "_csrf_token"] ["f6be13fd646962de676985ec9bb4a8d3", "qJ/aHzovZYpbrelGpRFec/cNlJyWjonXDoOMlDHbWzg="]
I also have a new cookie value now, unrelated to the one I edited.
So from this I think we can conclude is that what's happening is that since the cookie signature could not be verified, the session was nullified and regenerated. I now have a new session, with a different csrf_token.
The relevant code appears at actionpack/lib/action_dispatch/middleware/cookies.rb:460-464, in the EncryptedCookieJar class:
def decrypt_and_verify(encrypted_message)
#encryptor.decrypt_and_verify(encrypted_message)
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
nil
end
Rather than decrypting a message with an invalid signature, we just treat it as nil. So the unverifiable cookie that stores the session id and csrf token is not used to load the session, and anything that depends on the values in the cookie will fail.
So why didn't we get an error rather than just a new session? That's because we didn't try anything that depends on the encrypted values. In particular, although we have
protect_from_forgery with: :exception
(as opposed to :null_session) in ApplicationController, Rails does not verify the csrf token on GET or HEAD requests -- it relies on the developer to implement these actions according to spec, so that they're non-destructive. If you tried the same thing on a POST request, you'd get an ActionController::InvalidAuthenticityToken error (as you can easily verify for yourself).

Related

Where is the Session Stored in Rails?

In Rails, I have implemented the below code for user auth (confirmed to be correct). However, I wanted to confirm my thinking for this strange session[:session_token]. is this the "cookie" that is stored in the browser?
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :current_user, :signed_in?
private
def current_user
#current_user ||= User.find_by_session_token(session[:session_token])
end
def signed_in?
!!current_user
end
def sign_in(user)
#current_user = user
session[:session_token] = user.reset_token!
end
def sign_out
current_user.try(:reset_token!)
session[:session_token] = nil
end
def require_signed_in!
redirect_to new_session_url unless signed_in?
end
end
My understanding so far of how this works is that whenever the browser/client sends a request to rails, the cookie (with the session[:session_token]) is also sent over, thus allowing the current_user method to find the user. Is my understanding correct? This is strange to me because there's a gap of knowledge of how exactly the browser/client gets access to the session cookie when we declare it in ApplicationController (Rails-side).
You are pretty much there. Although, I have a feeling you might be confusing apples with oranges...
Sessions:
Very often in dynamic web sites one would want to store user data between HTTP requests (because http is stateless and you can't otherwise associate a request to any other request), but you don't want that data to be readable and/or editable on the client-side inside of the URL (like.. yourwebsite.com/yourPage?cookie=12345&id=678), and so on..., because you don't want the client to play around with that data without passing through your server-side code.
One way to solve this problem is to store that data server-side, give it a "session_token"(as you called it), and let the client only know (and pass back at every http request) that token. This is how the session is implemented.
Cookies:
The most common technique for implementing sessions in Rails involve using cookies, which are small pieces of text placed on the user’s browser. Because cookies persist from one page to the next, they can store information (such as a session_token or whatever else you want) that can be used by the application to retrieve the logged-in user from the database.
Where is the Session Stored in Rails?
Using both of the above concepts I can now tell you that the default session store inside of Rails is CookieStore, which is about 4KB in size.
To put it simply...
def sign_in(user)
#current_user = user
session[:session_token] = user.reset_token!
end
...method that you defined places the user into a temporary session.
Then the idea is that the following...
def current_user
#current_user ||= User.find_by_session_token(session[:session_token])
end
...method would find and retrieve the user from the database corresponding to the session token and initialize it to a variable you specified.
Additional info:
You should also note that there is an important difference between Rails's session and cookies helper methods...
They both generate cookies, however, session[...] method generates temporary cookies, which should expire upon the browser exit, and cookies[...] method creates persistent cookies, which do not.
Additionally, I would suggest having a look at Section 2 of Ruby on Rails Security guide. You might find it useful.
Hope this helps you out.
Session is stored in server side. And,
Cookie is stored in client side (in browser cookie). And,
When client/browser send a request to rails server, every time cookies are sent to rails server.
When a session is set in rails server, like: session[:user_id] = 4,
Rails store it in server side.
Session is saved in server side like key value pair (like json object)
For each browser, Rails set a session identifier in cookie, so that, Rails can find the correct session information for a request.
Without session identifier in cookie, Rails do not know, what session belongs to what browser.
So, session will not work without cookie.
Edit: Explain: sessions are stored server side
Suppose, I am using your web application, and after login I will be redirected to home page.
I open login page, input username and password, and click login button.
The form is submitted to sessions#login action.
in sessions#login - you check username and password - and set session[:session_token]:
if username and password is correct
random_unique_identifier_string = #user.remember_token
session[:session_token] = random_unique_identifier_string
redirect_to root_url
end
When server run this code session[:session_token], server need an unique identifier for each browser session.
So, server generate an unique identifier for this browser, such as: abc123
Server set all session variables in a place (may be in some folder or in database), label this folder as abc123.
Now server send a cookie request to browser - to set cookie _ebook_session = abc123.
(I see, if my app name is ebook, in rails, cookie name is like: _ebook_session)
Now the page redirect to home page.
** Note: Everything above happen in single request **
Now, in my browser, I want to open some page that need authentication (suppose, dashboard page).
You added before_action: require_signed_in! in dashboard controller.
So, when I open dashboard page in my browser, browser by default send all cookies with every request. so _ebook_session cookie is sent to server. Your server gets the value of _ebook_session cookie is abc123. Now your application know we need to look in abc123 folder for session. Now you can get value of session[:session_token] from abc123 folder.
** I have explained second request above **
Each browser needs unique session identifier.
Important: _ebook_session cookie will be set in browser in first request. If we already have _ebook_session cookie set in a browser, we do not need to set it again, second, third and next requests in that specific browser.
I hope, you understand.

messaging from the model level to controller level for ajax purposes

I have a page called /examples/1/duplicate
on there is an ajax form which kicks off /examples/1/run_duplicate
the controller method for run_duplicate runs Example.duplicate(session)
I pass in the session, which I understand is a holy no-no.
the duplicate model code is roughly:
def duplicate(session)
session[:duplicate] = 0
duplicate_foobars
session[:duplicate] = 1
duplicate_snafus
session[:duplicate] = 2
duplicate_widgets
session[:duplicate] = 3
end
I have another controller method on the duplicate page which is doing a long poll: /examples/1/check_dupe_status
The purpose was to get the updated status from session[:duplicate], tweak a progress report,
and report it back to the user via ajax so they could see the progress of Example.duplicate() .
sessions did not update the way I hoped. I can see the code run, but the sessions don't update, so /examples/1/check_dupe_status never knows that anything has kicked off.
Given that I did this wrong from the start, what is the correct way to notify a user about the state of Example.duplicate() ?
Sounds like you've got two separate requests, with one writing to the session and one trying to read from the session concurrently. Is that correct? Stop reading if it's not.
This can not work because the "session" is just a cookie - a header on the HTTP response that's downloaded to your browser as a cookie, then re-uploaded on the next request, then re-downloaded on the next response, ad nosium. In your code, here is the order of operations:
/examples/1/run_duplicate writes "0" to what is essentially a Ruby Hash, representing your session cookie
/examples/1/check_dupe_status reads the values from the session cookie that was just sent from your browser with this request. It probably didn't have anything at all in :duplicate, so it will appear as blank.
/examples/1/run_duplicate writes "1" to the Ruby session Hash
/examples/1/check_dupe_status reads, again, the session cookie which that request sent - nothing has changed
/examples/1/run_duplicate writes "2" to the Ruby session Hash
/examples/1/check_dupe_status reads from the session cookie it originally sent - no change
/examples/1/run_duplicate writes "3" to the Ruby session Hash and the request finishes, sending the session back as a cookie with a value of 3 at :duplicate.
/examples/1/check_dupe_status is still sitting there like a dufus, reading the blank session cookie it originally sent
At some point, /examples/1/check_dupe_status may timeout, and it may return with the session cookie. But guess what? Since :duplicate was never set in that session cookie, it will overwrite the one in your browser, and :duplicate will be blank in the next request you send.
Hopefully I expressed that clearly. Basically you're hitting a race condition in your cookies, and that's very difficult to overcome if you're sending concurrent requests from the same browser.
The best way to handle this would be to write your duplicate 0,1,2, etc. values to some database table. Your long-polling request could then just read out of the database. A little less efficient perhaps, but of course it has the advantage that it could work.

Setting Session Variables with AJAX and Rails when appcache is present

Have an ajax call to "updateUser" which does this:
puts session[:user_id]
user = User.find(params[:user_id])
if user
session[:user_id] = user.id
session[:user_name] = user.first_name + " " + user.last_name
puts session[:user_id]
render text => "Success.
end
The first puts shows the original user_id and the second shows the new user_id, so it would appear to be working properly. However, when I navigate to another page, all the session information is still that of the original user_id. What have I done wrong?
I have a feeling it has something to do with the local session cookie not being updated.
UPDATE
Definitely has something to do with caching. I can go to the page, clear the browser cache (am using Chrome as my browser), then run the ajax call and it works properly once. After that I am locked in to the (new) old user again.
UPDATE 2
Looks like it is something specifically to do with html5 application-cache. If I kill the appcache or run the script from a page that does not include manifest it works just fine. Still can't get it working properly on the cached page.
The same session id is being sent to the server from the cached page as the non-cached page, and the response headers are identical. But each request from the locally cached page causes the server to start with old session information.
http://diveintohtml5.ep.io/offline.html
I can tell that you've got a manifest caching problem, and altering the session itself is not going to clear the manifest. The cache is persistent until such time as the cached item is de-cached or the manifest is invalidated.
Another user ran into this same issue in a different way: they passed their session data in the URI and ended up caching a new application each time the user visited. Their solution may be useful:
How to clear Application cache (HTML5 feature) using JavaScript?
You might also take a look at this, on the various storage caches:
http://sharonminsuk.com/blog/2011/03/21/clearing-cache-has-no-effect-on-html5-localstorage-or-sessionstorage/
And finally, a resource on updating a cache file with JS:
http://www.html5rocks.com/en/tutorials/appcache/beginner/
This last one I would use after checking if the session ID has changed: update the session ID, then confirm the change, then clear and re-download the cached files.
Good luck. I hope that helps some.
The problem is that the session information is stuffed inside the application cache somewhere, All requests sent to the server are sent using that session info (which was cached on page load). So, we need to update the application cache with window.applicationCache.update() after the successful ajax call. This will cause the next request sent to the server to have the updated session information and all is well.
$.ajax({url: "/contoller/update_logged_user",
data: {id: user_id},
success:function(){
window.applicationCache.update();
}})
I encountered a very similar problem... had some code to store a user's zip code in session[:zip] when provided with an ajaxSubmit'ed form. Modified the implementation only slightly and suddenly session[:zip] had amnesia. Storing the info in cookies[:zip] worked properly. Path of least resistance.
Would you try setting the session[:user_id] = nil before assigning it with another value user.id and see what happens?

Rails 3 Cookie Based Sessions Question

With Rails 3, the default session storage mechanism is cookie_store. I assume that this means that the contents within the session hash are serialized, encoded and stored within a cookie in the browser? Does this mean that nothing (or very little) of the session is stored in the server?
I've had a few issues where I had a cookie overflow error and I'm assuming because I kept on adding to my user instance (which was also linked/fetched from the cookie).
u = session[:user]
u.add_this lots_of_data
so eventually I got a cookie overflow error.
Am I correct about this? Are sessions fully stored within cookies in Rails 3 (by default)?
Yes, if you use the cookie store, the session data is stored in the cookie. If you'd like to store it on the server, you will need to use another session store.
However, if you are storing model objects or "lots of data" in the session, you are most likely doing it wrong in the first place. Your data should go to the database, and the session should only contain as much information as you need to retrieve it.
In you case, this would mean to store the user id int he session, and load the user from the db in a before_filter.
Yes, you are right. The problem might come up if you keep on adding data to session.
But there are some other things that affect it.
Once, I ended up with CookieOverflow error, and the reason was the flash[:notice] messages.
If you use flash[:notice] = "message" and then redirect, the text "message" will be stored in the cookie. If the size of the text u pass is more than 4KBs, you get "CookieOverflow" error

Prevent Ruby on Rails from sending the session header

How do I prevent Rails from always sending the session header (Set-Cookie). This is a security problem if the application also sends the Cache-Control: public header.
My application touches (but does not modify) the session hash in some/most actions. These pages display no private content so I want them to be cacheable - but Rails always sends the cookie header, no matter if the sent session hash is different from the previous or not.
What I want to achieve is to only send the hash if it is different from the one received from the client. How can you do that? And probably that fix should also go into official Rails release? What do you think?
Rails only adds the session cookie data to the Set-Cookie header if it has been touched. You might be setting things to the values that they already contain - it's not smart enough to check to see if the data is actually different.
edit My response is a little misleading. When you are using the cookie session store, a new cookie is set if the cookie value (after Marshaling) changes.
See actionpack/lib/action_controller/session/cookie_store.rb
For Rails 3 then use this.
env['rack.session.options'][:skip] = true
or the equivalent
request.session_options[:skip] = true
You can find the documentation for it here http://doc.rubyists.com/rack/Rack/Session/Abstract/ID.html
Here is the crucial line:
config.action_controller.session_store = :nil_session_store
See the whole post.

Resources