1. 17
  1.  

  2. 3

    What I’d like to know is why does Chrome make that two minute exception? I wonder if it is just a hack to work around some google product breaking without it - perhaps some kind of sso or even a tracking cookie - and instead of fixing their own bug, they just modified the browser.

    1. 1

      I’d love to know that too. My guess is that their monitoring during the roll-out of the feature highlighted some popular system that was outside of Google’s control - so they couldn’t go and get the responsible team to fix it - but that had a big enough footprint that it was breaking things for large numbers of Chrome users.

      1. 1

        Found a clue in https://www.chromium.org/updates/same-site/faq

        This is a specific exception made to account for existing cookie usage on some Single Sign-On implementations where a CSRF token is expected on a cross-site POST request. This is purely a temporary solution and will be removed in the future.

        I think it’s a SAML thing: https://shibboleth.atlassian.net/wiki/spaces/DEV/pages/1181253974/IdP+SameSite+Testing and https://www.resolution.de/post/update-identity-provider-samesite-flag-cookie-google-chrome/

    2. 2

      I am desperately holding out for the day I can rip out form-based CSRF protection in favor of samesite and freinds. CSRF is super fidgety and at least in Django I have seen users hit CSRF failures in many different odd scenarios that I have just been completely incapable of debugging.

      All this to do server-side verification of what is basically a client-side concern…

      1. 2

        Depending on your appetite for risk it’s probably OK to do that now, if you’re using SameSite=Lax.

        I’m paranoid about people who use an older browser (while visiting a relative with an ancient Windows XP desktop perhaps?) but the actual chance of that happening AND someone managing to hit that user with a targeted CSRF attack feels incredibly slim to me.

        1. 3

          Serve your site using TLS 1.3+ only and you can use SameSite=Lax without worrying about support. Old browsers, that do not support that option will not be able to view your site anyway.

          1. 2

            A big reason I’m keeping CSRF tokens for the time being is also just plain ol’ distrust to new security methods. SameSite isn’t exactly new, but wide-spread deployment (without CSRF tokens as a “fallback”) kind of is. Maybe there are subtleties that either I or the community at large doesn’t quite grasp yet? Who knows. It doesn’t hurt to use both for the time being: CSRF tokens are set up, work, and the maintenance cost is low.

            1. 1

              The problem is that “works” isn’t always true. We have had users hit CSRF tokens and maybe it’s purely our fault but in theory we are using out of the box Django for this and yet we have had users get in very weird scenarios where their cookie-side CSRF tokens simply were not refreshing leading to failures until they cleared their own cookies.

              We don’t have tens of millions of users either, but our users are “normal people” with work to do. It’s hard to dig into the problems.

              Of course, the argument about it being secure is legit IMO. Just tough to have users hitting errors and us being “lol sorry” about it.

              1. 1

                Yeah, that doesn’t sound great. I don’t use Django; I’m not sure why it uses cookies for this. I guess that a HttpOnly cookie is a bit better since you can’t read it if you have a script injection problem or something.

                I just use a hidden form element and to the best of my knowledge it’s never caused any issues (certainly not where they had to contact support and clear cookies). This seems “good enough” to me, especially if cookie-based CSRF solutions can break in non-obvious ways.

                1. 1

                  What’s in your hidden form element?

                  Django compares the hidden form field with the cookie to ensure the form was served to you in the first place.

                  I’m having trouble imagining how a CSRF hidden form field could work without an accompanying cookie. What’s to stop an attacker from generating the hidden form value themselves on your site and then copying that value into their attacking application?

                  1. 1

                    It’s <input type="hidden" name="csrf" value="$token_from_template">; the token is stored in database (cached in memory) and on POST requests it checks if form.token != database.token then oh_noes.

                    What’s to stop an attacker from generating the hidden form value themselves on your site and then copying that value into their attacking application?

                    I’m not sure if I follow? What this is designed to protect from is things like a form on an attacker’s website that tricks the user in to submitting a request to /change-password, /change-email or the like; because this re-uses the login cookie this will work, and now you’ve changed the password from someone else. SameSite protects against this by enforcing a same-origin policy on coooies, so the browser won’t send cookies if the request to site1.com came from attacker.com.

                    The attacker doesn’t have access to the token, your website, or its contents. For an attacker to be able to generate a hidden form value on a site they need to be able to trick the user in to running code (i.e. an XSS/script injection), and at this point they can do pretty much anything.

                    I’m not sure if there are reasons to strongly prefer one method or the other, using cookies sounds easier if you’re using React or the like, but from a security POV they’re equivalent as far as I know.

                    1. 1

                      So it’s a token that’s only valid for updating a particular database record?

                      I like that as a way to prevent two users from accidentally over-writing each other’s updates, but for CSRF I like having a general solution which works for all forms, even forms that might not correspond to some specific database record that is being updated.

                      With your mechanism, how do you generate and store tokens for forms that cause a brand new database record to be created?

                      Also, does this work to protect against login CSRF attacks? Those are the attacks where I create an account on a site with a username and password, then trick you into visiting a page with this HTML:

                      <form action="https://www.example.com/login" method="POST">
                        <input type="hidden" name="username" value="evilme">
                        <input type="hidden" name="password" value="Password-I-Created">
                      </form>
                      <script>document.forms[0].submit()</script>
                      

                      Then I trick you into saving private data on that website (since you assume you are signed into your own private account) which I can later steal because I know the account password.

                      I don’t know of any ways to protect against login CSRF that don’t use a CSRF token in a cookie.

                      1. 1

                        Sorry, I think I’m not explaining this very well. Perhaps a small (pseudo)-code example might clarify it:

                        # create table users(
                        #     username       text,
                        #     password       text
                        #     session_token  text
                        #     csrf_token     text
                        # )
                        
                        def get_current_user():
                        	return db.query('select * from users where session_token=?',
                        		cookie.session_token)
                        
                        def render_page():
                        	user = get_current_user()
                        	print(f'''
                        		<form method="post" action="/change-password">
                        			<input type="hidden" name="csrf" value="{user.csrf_token}">
                        			<input type="password" name="password" placeholder="New password">
                        		</form>
                        	''')
                        
                        def change_password():
                        	user = get_current_user()
                        	if http.method.verb == "post" && form.csrf_token != user.csrf_token:
                        		return 'error'
                        	user.update_password(form.password)
                        

                        As you can see, it works everywhere, on all POST requests.

                        There is no need to protect login forms; a CSRF attack relies on the user having the session cookie in their browser (i.e. already being logged in), and there isn’t any if you’re not logged in.

                        The form you posted is something completely different: that’s something someone injected in your site to send data to an attackers site, rather than the reverse. I don’t think either method will reliably protect against that? It’s also something you can protect against with a Content-Security-Policy header by the way.

                        Certainly how I understand it, CSRF is only about preventing unauthorized forms submitting data to your sites.

                        1. 1

                          OK I understand now! Essentially your CSRF tokens are still tied to a cookie, but it’s the cookie used to identify the current user - the token itself is then looked up in the user database table.

                          That totally works.

                          Login CSRF is an attack where someone forces one of your users to sign into your site using /their/ account - and hopes that the user won’t notice the “you are logged in as evil123” banner at the top of the page. Then if they can convince the user to store sensitive information they can extract it later. It’s a very obscure attack, but it’s why I like CSRF protection on my login forms.

                          Here’s a link to old paper that first taught me about login CSRF: https://simonwillison.net/2008/Sep/24/robust/

                          1. 1

                            Oh yeah, the session cookie is essentially always a given; I actually don’t think there’s any other good way to identify logged in users at all(?)

                            I hadn’t heard of the Login CSRF specifically before, but you could modify the same concept to also apply to login forms with a bit of effort. But with the SameSite cookie it’s now a bit of a moot point; I’m content just letting it be.

                            1. 2

                              Huh, you just made me realize that, unlike other flavors of CSRF, Login CSRF is an attack that isn’t defeated by the SameSite=Lax cookie parameter!

                              An unprotected login form is sent without any cookies at all, and the responding application then sets an auth cookie - so without extra CSRF protection an application will stay vulnerable even if it uses SameSite.

                              1. 1

                                Thought more about this and added notes to my article. Shirt version: you can protect against Login CSRF these days by checking the Origin request header, or by looking for any SameSite=Lax cookie on the incoming login POST request (since those won’t be transmitted from off-site firms).

                  2. 1

                    Looked into this and Django’s cookie-based strat (as opposed to session-based) seems to be used to allow for CSRF protection to work even for anonymous users.

                    If you don’t have the cookie, an attacker can load your site, load the app, get the CSRF token for anonymous user, then just use that in their “attack” (for example to get people to signup to your app).