Sinatra clears session on post
Asked Answered
B

6

20
enable :sessions
set :session_secret, 'secret'

post '/login' do
        session[:loggedInUser] = jsondata['username'].to_s
        puts session[:loggedInUser] + " is the session"
end

Everything is good at this point. When I read the session like this:

get '/debug' do
    session.inspect
end

Its all there. But here comes the problem. When I go for another post request later on:

post '/foo' do
    # do nothing
end

The session is cleared.

Why? Is this a bug?

EDIT

I have narrowed the problem down: I proxypass Sinatra through nginx, to http://app.local/backend - this is when the issue occurs. If I run Sinatra through http://localhost:4567 it all works as expected.

SOLUTION

Use Rack::Session::Cookie instead of the default enable :sessions:

use Rack::Session::Cookie, :key => "rack.session",
:path => "/backend"
# etc

from the Sinatra FAQ:

If you need to set additional parameters for sessions, like expiration date, use Rack::Session::Cookie directly instead of enable :sessions:

Breana answered 17/9, 2013 at 8:26 Comment(4)
What is your version of Sinatra?Lette
I'm seeing exactly this behavior on 1.4.3, fixed with SOLUTION (which should be the accepted answer, minus :key => "rack.session", :path => "/backend" which isn't required).Discuss
Just my two cents: this was happening to me because the Rack::Protection module was dropping the session. In my case it was the HttpOrigin that failed. You might find it helpful to enable logging on Rack::Protection to see this. I didn't manage to do it (I'm using Gollum and not sure where to set this options), so I hard-coded an exception in the call method of Rack::Protection::Base.Marietta
@Marietta you saved the day for me big time! You should post this as a separate answer!Nalepka
A
18

I was suffering from the same issue as you: sessions were being cleared on post.

I have no idea why this works, but this is my solution:

#enable :sessions
use Rack::Session::Cookie, :key => 'rack.session',
                           :path => '/',
                           :secret => 'your_secret'

I literally just replaced the enable :sessions bit with use Rack::Session::Cookie ... and now all is good in the world.

Arawak answered 8/10, 2013 at 15:18 Comment(5)
I loved ur comment "I have no idea why this works". Worked for me too. So i voted up.Chairmanship
The key, literally, has to be rack.session ... otherwise this doesn't work. Stumbled over this, thought I'd leave a note.Affirmation
Thanks. That saved me from insanity today. I think the path is the key? Were you running your app under a different path?Nataline
Someone can please explain WHY ?Vanhouten
Explained why in my answer :) https://mcmap.net/q/622013/-sinatra-clears-session-on-postVanhouten
P
6

After I add set :session_secret, SESSION_SECRET, everything works.

set :session_secret, SESSION_SECRET
enable :sessions

Then I find, Sinatra's README does mention about that:

To improve security, the session data in the cookie is signed with a session secret. A random secret is generated for you by Sinatra. However, since this secret will change with every start of your application, you might want to set the secret yourself, so all your application instances share it:

set :session_secret, 'super secret'

Phthisis answered 26/8, 2016 at 7:52 Comment(0)
V
3

This happens because Sinatra regenerates the session cookie on every start of the application, if you run behind apache or a rack server that can start or switch to another instance you will face this problem.

The easier solution is set the secret to a fixed value with something like:

 set :session_secret, "328479283uf923fu8932fu923uf9832f23f232"
 enable :sessions

The other answer which suggest do this:

#enable :sessions
use Rack::Session::Cookie, :key => 'rack.session',
                           :path => '/',
                           :secret => 'your_secret'

Also work, but just because its setting the secret to a fixed value.

Vanhouten answered 9/9, 2016 at 19:37 Comment(1)
Fine to do this in development, but please don't do it in production! Read this article for more info: martinfowler.com/articles/session-secret.htmlSadler
S
1

Apologies for the necro-post but I have recently had this exact issue and none of the above suggestions correctly addressed my issue. Arnold's post is a potential problem as well but wasn't my issue and I already had a secret provided. I also couldn't find this issue documented anywhere any of my Google searches would find (but I did find this question), so hopefully can help those coming after me.

EDIT: on re-reading the entire page I can see Yuval is referring to exactly the same issue. I really wish I'd noticed this comment earlier:) Anyway I believe my answer still helps in giving a bit more context and how to fix things

This has been bit painful and given others above are asking why? I'm going to document what the root problem was and how it was caused. However, for the impatient amongst you: tl;dr you need to add the nginx directive proxy_set_header Origin http://$host; inside the location block for the app routes. The virtual host file should look something like this:

upstream puma_example {
  server unix://var/www/.../puma.sock fail_timeout=0;
}

server {
  server_name example.com;
  root /var/www/example.com/.../public;
  access_log /var/www/example.com/.../nginx-access.log;

  location / {
    try_files $uri @app;
  }

  location @app {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;
    proxy_set_header Origin http://$host;
    proxy_pass http://puma_example;
  }

Adapted from this page

Now for the why!

Primary explanation

Sinatra uses a sub-library called rack-protection. This protects your project against a lot of the varieties of internet badness. There's a base class that does most of the work and various subclasses for each type of check done. The one we're interested in is HTTP Origin.

The subclass overrides the accepts? method, as in fact do all the subclasses for the different attack types. This method (in this subclass) is called to determine whether the request is safe from a Cross-Site Request Forgery. The method will return true if the request is good, and false if it's a CSRF problem

The first line of the accepts? method calls the safe? method from the base class. Now we can see why GET requests sail through without an issue: if the request method is one of GET, HEAD, OPTIONS or TRACE then the request is marked as safe and the CSRF check evaluates to true.

But that doesn't cover POST requests, so we have to check the rest of accepts? method to see what other checks are applied. There are two further ways we can get the check to evaluate to true:

  1. Make sure we have the internet header HTTP_ORIGIN in our request, with the same value as base_url(env)
  2. Make sure we have the internet header HTTP_ORIGIN in our request & have the origin listed as one of the permitted origins

I went with option 1, not least because messing around with permitted options would have meant moving away from the default Sinatra sessions (I think!). As it turned out, I didn't have HTTP_ORIGIN set at all in any of my requests anyway, which would need to be solved for route 2. However by simply adding the proxy_set_header directive noted above, it does get set.

Don't forget to reboot Nginx service after you've made config changes!

N.B. Nginx prepends the HTTP_ part & upcases it. That's why I get away with calling it Origin in the directive


More detailed explanation

If you've not had enough fun geeking out github repositories yet, there's also the exact means by which accepts? method evaluating to false then de-rails your app:

  • The protection is initialised in the Base Sinatra class. The default reaction is set to :drop_session towards the end of this method
  • When the protection is called, it check the accepts? method discussed above
  • When false (because of the missing headers already discussed), the react method is called
  • react, in turn calls the reaction, which was set to :drop_session when the protection was initialised
  • drop_session calls session.clear, which takes us into rack proper. Unfortunately rack has recently been refactored to remove sessions from the main code base, and move it to rack-session, but my version of Sinatra is still relying on 2.2.4 of rack so we need to look at old commit files
  • The clear method can be found in the session/abstract/id.rb file
  • clear calls load_for_write!
    • load_for_write! calls load!
    • load! calls load_session
    • load! takes the decoded session data & sets it to the @data class variable
    • load! finally sets the class variable @loaded to true, which ultimately stops subsequent load_for_read! & load_for_write! calls from loading the data from the session cookie again
  • clear then empties the class internal variable @data, but crucially leaves @loaded set to true

Therefore after a session.clear call we have a session object that is loaded (so won't ever reload data from the session cookie) but has no data in it.

...and this is why it looks like POST requests are dropping the session cookie. The session cookie was getting to Rack / Sinatra all along. However with the wrong headers the session data was read in, and then wiped, and then set so it would never re-read in the data.

Sadler answered 5/10, 2022 at 16:49 Comment(0)
M
0

At the request of @smoyth I am posting my comment as a separate answer.

Just my two cents: this was happening to me because the Rack::Protection module was dropping the session. In my case it was the HttpOrigin that failed. You might find it helpful to enable logging on Rack::Protection to see this. I didn't manage to do it (I'm using Gollum and not sure where to set this options), so I hard-coded an exception in the call method of Rack::Protection::Base.

Marietta answered 20/12, 2021 at 9:4 Comment(0)
L
-3

I do not see any issues at all. Here is my code. Try this and see if you still have that issue.

require 'sinatra'

configure do
  enable :sessions
  set :session_secret, 'secret'
end

get '/login' do
  session[:foo] = Time.now
  "Session value set."
end

get '/fetch' do
  "Session value: #{session[:foo]}"
end

get '/foo' do
  "Session value: #{session[:foo]}"
end

get '/logout' do
  session.clear
  redirect '/foo'
end

http://localhost:4567/login #=> Session value set.
http://localhost:4567/fetch #=> Session value: 2013-09-17 09:42:56 +0100
http://localhost:4567/foo #=> Session value: 2013-09-17 09:42:56 +0100
http://localhost:4567/logout #=>(redirects to) http://localhost:4567/foo #=> Session value:
Lette answered 17/9, 2013 at 8:42 Comment(4)
Thanks, your code is fine. I have narrowed the problem down: I proxypass sinatra through nginx, to "app.local/backend" - this is when the issue occurs. If I run Sinatra through localhost:4567 it all works as expected.Breana
I'm seeing this issue too, but only with post requests, which you answer doesn't address...Pettish
In above code there is no post requsest. What is the solution for post?Skelp
This answer doesn't answer the question. The title is "sinatra clears session on POST". Your answer doesn't address post requests.Calender

© 2022 - 2024 — McMap. All rights reserved.