Skip to content
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

Add force_new parameter to generate_csrf #354

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

jnschaeffer
Copy link

Security best practices indicate CSRF tokens should be unique for each user session. Adding a force_new parameter to generate_csrf enables users to generate new CSRF tokens between user sessions with minimal code changes or understanding of CSRF generation internals required.

Security best practices indicate CSRF tokens should be unique for each
user session. Adding a force_new parameter to generate_csrf enables
users to generate new CSRF tokens between user sessions with minimal
code changes or understanding of CSRF generation internals required.
@codecov-io
Copy link

codecov-io commented Jan 10, 2019

Codecov Report

Merging #354 into master will increase coverage by <.01%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #354      +/-   ##
==========================================
+ Coverage   99.67%   99.67%   +<.01%     
==========================================
  Files          18       18              
  Lines         911      919       +8     
  Branches       74       74              
==========================================
+ Hits          908      916       +8     
  Misses          3        3
Impacted Files Coverage Δ
flask_wtf/csrf.py 98.07% <100%> (ø) ⬆️
tests/test_csrf_form.py 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9b029c3...1a2940a. Read the comment docs.

@jnschaeffer
Copy link
Author

As an aside, is there an upcoming release planned for Flask-WTF at some point in the future?

@brannondorsey
Copy link

We recently had to roll this behavior ourselves in a project using flask-wtf. In some cases, failing to rotate a CSRF token on website login can lead to CSRF fixation attacks, whereby an attacker knowingly sets the victim's CSRF token before the victim logs in. Once the victim has logged in, and the CSRF token doesn't rotate, the attacker can force the victim to make state-changing requests to a web application without their knowledge and against their will. This feature would be greatly appreciated!

@davidism
Copy link
Member

davidism commented Jan 10, 2019

I don't understand why this is necessary. If a user clicks "log out", the logout route should clear the session, at which point a new token would be generated next time anyway. Same with if a user logs in and the session isn't empty. Teaching users to call this function with this argument in these cases is exactly as much work, but less comprehensive, than teaching them to do session.clear().

@davidism
Copy link
Member

davidism commented Jan 10, 2019

I'm also not clear how you'd expect this to fix session fixation, as it doesn't prevent an attacker reusing a previous session cookie as long as it's not expired. The way you protect against that is with HTTPS, not with CSRF. Note that Flask's secure cookie already prevents tampering with the data within the cookie.

@brannondorsey
Copy link

This doesn't fix session fixation, it fixes the possibility of CSRF fixation: where an attacker knows, or is able to set, the victim's CSRF token. The OWASP CSRF cheat sheet recommends CSRF tokens are unique to each user session.

A CSRF token should be unique per user session, large random value, and also generated by a cryptographically secure random number generator.

Imagine the following scenario:

  1. An attack loads example.com/login and records the CSRF token
  2. The victim logs into the example.com using the same browser
  3. The victim is tricked into following a malicious link provided by the attacker, who, equipped with the known CSRF token, can make state-changing requests on behalf of the user without their permission.

This attack scenario becomes far worse if other factors lead to the attacker being able to set cookies on the victim's browser:

  1. An attacker lures a victim to attacker.example.com and sets a session cookie containing a CSRF token scoped for example.com in the victim's browser.
  2. The victim later signs in to example.com/login, logging in with the same CSRF token known by the attacker.
  3. The victim is tricked into following a malicious link provided by the attacker, who, equipped with the known CSRF token, can make state-changing requests on behalf of the user without their permission.

The second attack is obviously much worse, as it doesn't require physical access to the machine, but I think we would both agree that failing to rotate a CSRF token on login can have serious security implications.

@davidism
Copy link
Member

Could you provide an example of how this change would be used to prevent that?

Scenario one is more comprehensively addressed by just clearing the session on login and logout. The session cookie is not vulnerable to step one in scenario two because it is securely signed.

@tarqd
Copy link

tarqd commented Jan 10, 2019

An example of an exploitable scenario:

  • I own blogglr.com and provide blogs to my users
  • Users can control the content of username.blogglr.com
  • Attacker tricks the user into visiting their blog and set their session/csrf token cookie to a known value (attackers own session/csrf cookie) with the domain set .blogglr.com. Setting the cookie path can also be used to force higher precedence than existing session/csrf cookies
  • If the User logs in while using this attacker known session/csrf token, the session becomes authenticated and the attacker can now use this knowledge to execute CSRF attacks against the user

Note that even if the session id changes, if the CSRF token does not rotate upon principal change (anonymous -> authorized user), the attacker will not have access to the authenticated session but will now have knowledge of the CSRF token and can use this to execute privileged commands as the user.

@jnschaeffer
Copy link
Author

jnschaeffer commented Jan 10, 2019 via email

@davidism
Copy link
Member

That is addressed by calling session.clear() on logging in or out, this change doesn't add anything to that.

@tarqd
Copy link

tarqd commented Jan 10, 2019

It does make it easier for users to safeguard their applications without having to change the behavior of their application for users that using flask-login or similar libraries though.

In order to make their application safe and maintain the behavior they expect from flask login where only the user-id and session-id are cleared they'd need to:

  • Copy all the properties of the anonymous session except the CSRF token related ones
  • Clear the session
  • Copy all the properties back into the session

Alternatively, they would have to rely internals to rotate the CSRF token.

I haven't dived deep into the codebase so I might be wrong here but wouldn't the user also need to clear g.field_name in order for the rotation to be effective?

It looks like it should be okay as long as you call session.clear before calling generate_csrf

@tarqd
Copy link

tarqd commented Jan 10, 2019

tl;dr I don't disagree with you that session.clear is a valid way to mitigate vulnerabilities, it's just a round-about way of accomplishing the task (we're destroying the whole session in order to rotate the CSRF token).

@davidism
Copy link
Member

davidism commented Jan 10, 2019

It sounds like a clear_csrf() function that only removes the CSRF key and nothing else would be more clear. Then the logout view becomes:

@app.route("/logout")
def logout():
    clear_csrf()
    logout_user()
    return redirect(url_for("index"))

I personally haven't encounterd an app where logging out would still want to preserve something in the session, and if it does it's probably more appropriate for Redis or the database. In the majority of cases where that's not needed, session.clear() is more useful. In the cases where it's needed, then you probably still have some subset of things you do vs do not want, so you'd still need some logic to copy what you do want, at which point the CSRF is still cleared without this.

@app.route("/logout")
def logout():
    logout_user()
    for key in session:
        if key not in keys_to_keep_for_my_app:
            del session[key]
    return redirect(url_for("index"))

@jnschaeffer
Copy link
Author

The motivation for adding this as a parameter to generate_csrf rather than as a separate clear_csrf function is that on login, rather than clearing the CSRF token if one exists and then setting one a package user can generate a CSRF token in a single function call and guarantee the generated token is fresh.

Providing this is less about good session management within Flask and more about good CSRF token management within Flask and other Flask extensions. Flask's documentation demonstrates popping values off the session during logout (see here), while Flask-Login only mutates the fields it's concerned with on login and logout, so adding the ability to rotate only the token in the session allows for improved CSRF protection without deviating from common practice within the Flask community.

@tarqd
Copy link

tarqd commented Jan 11, 2019

@davidism

I personally haven't encounterd an app where logging out would still want to preserve something in the session,

One example would be a shopping cart, where the user can add items to their cart when logged out but need to login to pay. You'd want to maintain the shopping cart in the transition to an authenticated session. I can imagine a few other scenarios as well (session-bound preferences like sort order, filters for searches, etc).

It sounds like a clear_csrf() function that only removes the CSRF key and nothing else would be more clear. Then the logout view becomes:

I actually was about to suggest making this a separate function as well! I think it makes it more clear what's going on, especially since generate_csrf implies you are using the return value but clear_csrf makes it more clear of the intention imo.

I'd also add that in either case, that in order to ensure the function is used properly, it's essential that the the clearing function (clear_csrf or generate_csrf(force_new=True) happens before any other calls to generate_csrf, otherwise there's potential that parts of the application are using the expired CSRF token.

I'd recommend that the CSRF token only be cleared from the session if g.field_name has not yet been assigned, implying that the CSRF token to be rotated has not been used yet. Otherwise, it should throw an error (csrf token can't be cleared because it's already been used)

@tarqd
Copy link

tarqd commented Jan 11, 2019

It's also nice that any authentication libraries that are aware of flask-wtf can use clear_csrf to provide this protection without the application needing to deal with it since they are aware of principal changes. They could also clear the session and copy but this makes it clear what they need to do and is easy to document.

Plus if you add the safe-guard against rotating after already calling generate_csrf, it helps prevent a potential foot gun.

As an aside, I apologize if I came off as short in my earlier comments, the caffeine crash is hitting hard 😅 . All your work on this project is much appreciated

@jnschaeffer
Copy link
Author

Aclear_csrf function is a perfectly fine idea too, now that I'm thinking about it further! I like that packages which are aware of Flask-WTF could clear tokens without necessarily needing to generate new ones, per @ilsken's suggestion, and more generally like the idea of signaling to package users what best practices are when handling CSRF tokens between user sessions.

Either way works for me; the package has been great to use so far.

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

Successfully merging this pull request may close these issues.

None yet

6 participants