New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amazing post that aroused me a question. #10

Open
ghost opened this Issue May 6, 2018 · 12 comments

Comments

Projects
None yet
1 participant
@equivalent

This comment has been minimized.

Copy link
Owner

equivalent commented May 11, 2018

Hey @johnunclesam thank you for you post. I'm glad you like my article.

Sorry that I didn't answer the question earlier but I was hammered with work and unfortunately I'll not have time to fully answer it this weekend either (I'm on a trip)

I'm bit rusty on CSRF topic and need to remind myself on the topic before I can give valuable answer (as security answers are always complicated)

In short I would say just this: I'm using protect_from_forgery with: :exception for our SPA API

I'll fully explain myself once I'm back but here are some random resources that may help:

b.t.w. the new url of the article is https://blog.eq8.eu/article/csrf-protection-on-single-page-app-api.html
I changed the blog to Jekyll so I can write articles faster. Sorry for the problems

@equivalent

This comment has been minimized.

Copy link
Owner

equivalent commented May 11, 2018

Ah ok my bad now I understand what you are asking

it really depends on how the SPA is being rendered.

scenario 1 - Rails rendering the SPA

e.g. angular-rails gem rendered via sprockets

So rails render application.html.erb with angular template and then the Angular JS just communicates with Rails via API

|-------------------|
| Rails             |
|    |-----------|  |
|    |SPA        |  |
|    |__________ |  |
__________________

Scenario 2 -SPA that is rendered outside Rails

(e.g. GitubPages Angualar JS App that is just communicating with pure Rails API, so Rails has nothing to do with redering the SPA)

 |--------|             |-------------|
 | SPA    |             | Rails          |
 |________|             |_________|

Solutions

It's 23:15 when I'm writing this response, I need to go to bed soon to wake up in few hours for a long road trip 😄 ...I'll update my answer once I'm back, this is just quck B.S. top of my head

For scenario 1

I kind-of remember that one old project I use to work for had similar solution like you are proposing https://stackoverflow.com/questions/50159847/single-page-application-and-csrf-token :

class ApplicationController < ActionController::Base

  after_action :set_csrf_cookie

  def set_csrf_cookie
    cookies["X-CSRF-Token"] = form_authenticity_token
  end

end

So I guess that's fine + protect_from_forgery with: :null_session but will reply on that later.

For scenario 2

Let me check something will reply back on Monday,

I would consider returning the CSRF token in the response header rather than setting it to cookie 🤔 so response.set_header('HEADER NAME', 'HEADER VALUE') so after that SPA can store it for next request in local storage rather than cookie (maybe better as GDPR makes cookies hell )

@equivalent

This comment has been minimized.

Copy link
Owner

equivalent commented May 11, 2018

...now I'm not sure how will I fall asleep tonight with this in my mind :)

@ghost

This comment has been minimized.

Copy link
Author

ghost commented May 30, 2018

Everything ok in your trip?

@equivalent

This comment has been minimized.

Copy link
Owner

equivalent commented Jun 14, 2018

@johnunclesam sorry for the 2 week delay. I forgot about this.

I've formed my answer into this article :) https://blog.eq8.eu/article/rails-api-authentication-with-spa-csrf-tokens.html

Thx for the inspiration

@ghost

This comment has been minimized.

Copy link
Author

ghost commented Jun 15, 2018

Fix the word <mata> and this …) in the article.

@ghost

This comment has been minimized.

Copy link
Author

ghost commented Jun 15, 2018

Great article. But anyway I would like to ask something.

Many times I see this:

class ApplicationController < ActionController::Base

  after_action :set_csrf_cookie

  def set_csrf_cookie
    if current_user && protect_against_forgery?
      cookies["my_csrf_token"] = form_authenticity_token
    end
  end
end

It makes sense to have the code below?

if current_user && protect_against_forgery?
@equivalent

This comment has been minimized.

Copy link
Owner

equivalent commented Jun 20, 2018

@johnunclesam I've never used it like that so I cannot make statement that I would be 100% sure of.

In principle I cannot think about scenario where your code would be problem

But at the same time I don't see a problem to set the CSRF cookie on every request like this:

class ApplicationController < ActionController::Base

  after_action :set_csrf_cookie

  def set_csrf_cookie
      cookies["my_csrf_token"] = form_authenticity_token
  end
end

🤔

honestly don't know. I definitely see what you are trying to archive but both solution (your and my code sample ) looks equally fine to me (but I may be wrong on this)

but just to be sure protect_from_forgery should be on globally.

class ApplicationController < ActionController::Base
   protect_from_forgery
end

and honestly there is no case I can think of where would developer need to do skip it with protect_from_forgery except: :create or skip_before_action :verify_authenticity_token. If that is the case why you use protect_against_forgery? in your code then you may have some security issues in your code.

@ghost

This comment has been minimized.

Copy link
Author

ghost commented Jun 23, 2018

I think you're right.

It does not make sense to use if current_user && protect_against_forgery?.

@ghost

This comment has been minimized.

Copy link
Author

ghost commented Jun 24, 2018

But maybe the if current_user can help prevent CSRF Breach attack? What do you think?

@equivalent

This comment has been minimized.

Copy link
Owner

equivalent commented Jun 27, 2018

well yes and no. CSRF Breach attack happens when attacker manage to figure out CSRF token from website compression (and yes even if the trafic is HTTPs)

Quoting this and this article

Under certain circumstances, attackers could figure out a victim’s secret token even when the communication was encrypted. Specifically, the attacker could discover information about the token because of the way that a Web page gets compressed.

Compression is a powerful way to speed up communication because repeated sections of text only need to get transmitted once. For example, if the word “Beetlejuice” is repeated three times in a Web page, the second and third instances get compressed into tiny shortcuts that refer to the first word. Similarly — and unfortunately — if any letters in the Web page match some of the letters in the CSRF token, compression makes the Web page smaller. This size difference is still present after the Web page is encrypted, and this effect could be used to reveal information about the CSRF token to any attackers who can see the size of the compressed Web page. A smart attacker could use a few hundred carefully crafted Web requests to figure out the entire token from the size alone.

That means that yes not exposing the CSRF token for every request (only for logged in users) is little bit helpful but still doesn't solve the issue (as attacker can still figure out CSRF token of the current_user)

Therefore if your website is dealing with this level of concern around security you will have to rotate CSRF tokens. Quoting same articles:

Facebook protects people against this threat with an extra layer of security inside the CSRF token. Before BREACH was invented, the token was rotated once daily. The CSRF token contained a truncated SHA-2 hash that incorporated the account ID and current date. A person with three Facebook sessions within a single day would have received an identical CSRF token each time, (e.g., AQAOQ2sf, AQAOQ2sf, and AQAOQ2sf). Now our system replaces the token with a new one every time it is requested. Three different sessions use three different CSRF tokens, (e.g., AQGSRmYTeFnr, AQFkqN92V78v, and AQHouyYa35iv). These new tokens are generated by introducing a random 24-bit salt. The salt is the last four letters at the end of the token and is also included within the hash, which eliminates all repetition anywhere in the token. After a new token is issued, the previous tokens still remain valid for a couple of days, resulting in multiple tokens being permissible simultaneously

Therefore you are doing something like that (CSRF rotation) with:

class ApplicationController < ActionController::Base

  after_action :set_csrf_cookie  # <- this

  def set_csrf_cookie
    if current_user && protect_against_forgery?   
      cookies["my_csrf_token"] = form_authenticity_token   # <- and this 
    end
  end
end

So you are actually implementing 2 security steps here:

  • not exposing CSRF tokens to public users (if current_user) => decreasing the knowledge for hacker on how your CSRF tokens look like. This make sense if you are building "internal" application where not anyone can create an account 👍. But if I can visit your website and create an account then this is not necessarily effective 👎
  • rotating CSRF token with after_action :set_csrf_cookie and cookies["my_csrf_token"] = form_authenticity_token 👍 But the question is: is it as secure rotation as Facebook is providing 🤔 ?

So in conclusion

If you really need to bulletproof the security I would recommend investigating Rails source code around what CSRF token https://github.com/rails/rails/blob/e7feaff70f13b56a0507e9f4dfaf3ebc361cb8e6/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L263

It's not as sophisticated as Facebook is proposing but still provides some good tricks to prevent BREACH yet if I was a Bank and had to have bulletproof CSRF I would investigate a way how to add one more CSRF like token that is generated per user base + md5 stored in DB for individual users so I can keep track of individual tokens and if user signs out (or system detect a breach) I delete those from DB (so Rails CSRF + my custom user tracked CSRF just to be sure so I'm super paranoid)

If this CSRF protection is for my personal Blog website I wouldn't bother with that paranoid solution and just use Rails native CSRF.

But still your solution make sense. But one think you need to be extra careful is not to implement public forms without CSRF token that may be turned into protected one some day => you or your colleagues may forgot to remove skip protect_from_forgery this is in my opinion bigger risk :)

@equivalent

This comment has been minimized.

Copy link
Owner

equivalent commented Jun 27, 2018

🤔 actually @johnunclesam Thank you again, now I realize that I've forgot to mention that in the article. Updating now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment