Cookie Security for Flask Applications

Posted by
on under

Cookies are the most common attack vector for applications that run on web browsers, yet the topic of how to make cookies secure is frequently overlooked. I touched upon this topic in a few past articles, but today I want to specifically go over all the options Flask and extensions such as Flask-Login and Flask-WTF give you in terms of securing your application against web browser attacks.

Cookie Security

Use HTTP Encryption

Before you start looking into protecting against some of the sophisticated attacks browsers can be victims of, you have to make sure that you are protected against more basic vulnerabilities. And top among them is the sending of sensitive information over regular HTTP, which does not use encryption. Without encryption, session cookies (and passwords too!) are traveling through the network in near clear text, making any intermediaries potential attackers that can steal these cookies and use them to do bad things. I have blogged about Flask user session cookies and specifically about how easy it is to decode them without having the application's secret key, if you are interested in the details.

So how do you make sure that your web traffic is always encrypted when sent between the server and the client? Very simple, you have to use HTTPS. These days you can get an SSL certificate for your domain for free, so there is really no excuse to not have one in your production server. If you want to learn what it takes to implement HTTPS, I have blogged about that too.

HTTP to HTTPS redirects

A typical configuration for production sites is to redirect any requests that are sent over HTTP to the same URL but on HTTPS. If you are doing this, you need to make sure that these HTTP requests that are immediately redirected to HTTPS do not carry the Flask session cookie with them, or actually any cookie that contains sensitive information. You can do that by making sure your cookies have the secure flag set. The browser will never send secure cookies with requests that are not encrypted.

With Flask, you can control the secure flag on the session cookie with the SESSION_COOKIE_SECURE configuration setting. By default, it is set to False, which makes the session cookie available to both HTTP and HTTPS connections. This is a proper value for development, but on a production configuration you definitely want to change this setting to True.

Do you use Flask-Login? If you use the "remember me" functionality offered by this extension, that uses a separate cookie, in which Flask-Login writes a remember token. It's probably a good idea to also make that cookie secure on your production server. Flask-Login allows you to do that through the REMEMBER_COOKIE_SECURE configuration setting.

So to summarize, you want the following two settings added to your production configuration:

SESSION_COOKIE_SECURE = True
REMEMBER_COOKIE_SECURE = True

And with this, you can be sure that your cookies will never be sent on an unencrypted wire.

Browser Specific Attacks: XSS and CSRF

Web based applications typically use cookies to store authentication information that allows the user to freely navigate through the different pages of the site with their logged-in state preserved from one page to the next. In Flask applications, this state is typically written in the user session cookie. This is actually what the popular Flask-Login extension does.

If a malicious agent finds a way to steal this cookie from a client, then this attacker can potentially send requests to your application server impersonating the client, so from the server's point of view these requests will appear to be coming from the client as part of an existing logged-in session. Note that in this situation the attacker does not need the victim's password to gain access, having a valid session cookie is enough.

The two most common types of web browser attacks are called Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF). I have discussed them in a previous article titled Handling Authentication Secrets in the Browser, but if you want the 10,000 foot summary, XSS involves the attacker injecting malicious JavaScript code into your application, and CSRF involves the attacker luring your users into a site that sends malicious requests asynchronously to your server.

The forms of attack between XSS and CSRF are very different. In the case of XSS, the attacker physically steals your cookies using malicious JavaScript code. In the CSRF case, no cookies are stolen, but the attacker relies on the browser cookie policy to attach cookies set by your server. The end result in both cases is that your server receives requests from an attacker that come with a valid user session that belongs to one of your users.

Protecting against XSS

The best way to protect against XSS attacks is to set the httpOnly flag on any cookies that hold sensitive information. Browsers hide HTTP-only cookies from JavaScript, but they still send them with outgoing requests. Your application running on the browser will not be able to see or read these cookies (it does not need to anyway), and thanks to that, an attacker's injected JavaScript will not be able to access them either.

Luckily, Flask sets the httpOnly flag by default on the user session cookie. This flag is controlled via the SESSION_COOKIE_HTTPONLY configuration setting, and my recommendation is that you leave it with the default or set it to True for both development and production. The remember me cookie from Flask-Login, however, does not have this flag set by default, so you'll want to set REMEMBER_COOKIE_HTTPONLY to True in your configuration.

To summarize this section, add the following to both development and production configurations:

SESSION_COOKIE_HTTPONLY = True
REMEMBER_COOKIE_HTTPONLY = True

Protecting against CSRF

So here is where things get interesting. The easiest way to protect against CSRF is not to use cookies for authentication and user sessions, and instead have the application insert the user session or token in all requests in a custom HTTP header. That makes it impossible for an attacker's site to send a request that includes the user session, because this attack relies on the browser attaching a valid session cookie to the malicious request. But unfortunately, we've seen in the previous section that the best protection we have for XSS attacks consists on using cookies with the httpOnly flag enabled. So cookies are good for XSS and bad for CSRF!

If your project is an API, XSS may not really be a problem, since you will have no web based UI in which an attacker can inject malicious JavaScript. In this situation, using the Authorization or other custom header to send a token or user session is enough to protect you against CSRF.

But for many projects that have a web application it is going to be a major complication to not be able to rely on cookies for authentication. In this case, we are going to assume that the session cookie is going to be used, and with this choice we have a way to protect against XSS, but we are a potential target for a CSRF attack. So we need to look for a Plan B regarding CSRF, and that is the use of CSRF tokens.

A CSRF token is a randomly generated string that the server assigns to each client. The server passes this token to the client by some means, and then the client is supposed to send this token back to the server with any requests it sends. The server checks that this CSRF token is the correct one, and if it is not, it refuses the request.

If you are handling your web forms with the Flask-WTF extension, you are already protected against CSRF on your forms by default. You probably recall that when you create a form HTML template you have to add {{ form.csrf_token }} or {{ form.hidden_tag() }} somewhere inside your form markup, right? That takes care of inserting the CSRF token in your form as a hidden field.

If you are using asynchronous requests (i.e. ajax), you have to manually insert the CSRF token as a custom header in all requests that modify the state of the server, which typically means POST, PUT, DELETE and maybe PATCH. Here is an example Jinja template from the Flask-WTF documentation that shows how the server passes the CSRF token to the client's JavaScript, and then how the client inserts the custom header using jquery's ajax support:

<script type="text/javascript">
    var csrf_token = "{{ csrf_token() }}";  // the token is set by Jinja2

    $.ajaxSetup({
        beforeSend: function(xhr, settings) {
            if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
                xhr.setRequestHeader("X-CSRFToken", csrf_token);  // insert custom header
            }
        }
    });
</script>

Session Security

In spite of all the precautions we took to make our application secure, there is still the chance that an attacker can find a way to steal a session cookie. This can happen, for example, if your user is using a compromised computer. As discussed above, an attacker that uses a stolen session cookie can get the same access the user from who the cookie was stolen has, so this can end up being a major headache for your user and maybe even for you.

An easy way to mitigate the risk of such an attack is to attach some information about the client to the session, such as their IP address and the browser's user agent. Every time the server receives a request with a session cookie, it an checks if these client attributes match, and if they don't it can throw away the session, forcing the client to log in again. In particular, the IP address of a client is very difficult to spoof, so an attacker that steals a cookie and tries to use it from another location is going to be prevented from causing damage.

Flask-Login has an implementation of this idea when you set session protection to "strong":

login_manager.session_protection = "strong"

In this mode, Flask-Login will mark a user as logged out when it detects that an existing session suddenly appears to come from a different originating IP address or a different browser. But it is unfortunate that Flask-Login does not enable this option by default, and also does not support it at all if you want to also use the "remember me" functionality.

Since I consider the session protection support in Flask-Login incomplete, and have been unable to convince the author to improve it, I have now created a separate extension with the specific goal of protecting the session. The extension is called Flask-Paranoid, and you can install it with pip:

pip install flask-paranoid

Once installed, all you need to do is create an instance of the Paranoid() class and that will protect your session in the same way Flask-Login does in strong mode:

from flask_paranoid import Paranoid

app = Flask(__name__)
paranoid = Paranoid(app)
paranoid.redirect_view = '/'

With the above set up, any time the session is detected to come from a different IP address or user agent, the extension will block the request, clear the user session and the Flask-Login remember cookie (if found) and then issue a redirect to the root URL of the site.

Conclusion

As you see, it does not take a lot of effort to implement cookie security in a Flask application. I hope this was a useful article. Do you take other measures to secure your Flask application? Let me know below in the comments!

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

39 comments
  • #1 Murtuza said

    Thank you for the article, very helpful.

  • #2 WTRipper said

    Thank you, Miguel!
    The CSRF token is only valid for some time of course but how can the lenght of this time period be changed?

  • #3 Miguel Grinberg said

    @WTRipper: Set the token duration in seconds in the WTF_CSRF_TIME_LIMIT configuration variable. The default is 3600 seconds (1 hour).

  • #4 WTRipper said

    @Miguel: Thank you!
    Your Flask-Paranoid extension is a kind of fix for Flask-Login right? I just have to initiate it as you mentioned above and it will protect my sessions. Then I drop my LoginManager.session_protection strong setting of Flask-Login but I keep using Flask-Login for the rest (login_user, logout_user, login_required, login_view, current_user, UserMixin), right?

  • #5 Jarriq R said

    Is it possible to generate csrf token without wtf form?

  • #6 Miguel Grinberg said

    @WTRipper: Yes, you got everything right!

  • #7 Miguel Grinberg said

    @Jarriq: The example in the "Protecting against CSRF" section generates a CSRF token using Flask-WTF, but without using forms. This is the way you do CSRF protection for XHR requests.

  • #8 Tom King said

    This is amazing, thank you for this. Cookie security was something which I was never that comfortable with on Flask Login, so really excited to use this.

    Don't suppose you've ever come across problems with CSRF token failures for POST requests when using the remember_me token? Flask-WTF only seems to check the CSRF token in the session cookie, not the remember_me cookie. I'm about to do a hard-fix myself, but was hoping there would be a better way!

    Looking forward to receiving you upcoming 2017 Flask e-book!

  • #9 Miguel Grinberg said

    @Tom: Can you explain your concern in more detail? CSRF tokens are supposed to be short lived. The "double submit" style of CSRF token validation is appropriate, the client submits the token in the session cookie, and also in the form's hidden field. Do you want a POST request that is authenticated through the remember me cookie to be protected with CSRF? I would not do that myself, I would require the client to authenticate first, which for a regular HTTP form would happen in the GET request that returns the HTML page for the form.

  • #10 Tom King said

    @Miguel,
    Sure! So I'm using Flask-login, and Flask-wtf as two of my plugins. I've enable the option when logging in to "Remember me", so that when the user closes the browser and revisits the site, their login in now classes as "Non-fresh" (Functionality provided through Flask-login). I'm using alternative tokens here, so if the user does happen to change password, it would invalidate the session cookie and remember me cookie anyhow.
    I want to allow users to use POST requests here, as like with many other websites, so long as they have logged in previously, they should be allowed to use the websites functionality, until the expiry time of that remember me cookie.
    The CSRF token are in fact short lived, as by default they get regenerated on the hidden tag each request along with the session too.

  • #11 Miguel Grinberg said

    @Tom: I don't understand what the problem is then. CSRF and remember me features have nothing to do with each other. If a user that has the remember me cookie returns to your site, they will issue a GET request first, and that will create a non-fresh session with Flask-Login. The CSRF token will be written to the user session, regardless of the fresh/non-fresh state in Flask-Login. You must be doing something else that causes the session to be erased, maybe?

  • #12 Tom King said

    @miguel,

    Hmm, you may be right then - I didn't think they should have any interaction. Seems that when I've got a non fresh session, when attempting to get the users session it is empty. Time for some investigating! Thanks for the help, keep up your great work.

  • #13 Cheick B said

    I am planning on using your flask-paranoid extension in addition to the flask-kvsession and flask-login extensions that I am already using. Do you have any recommendations as to how I could integrate these three extensions together? I am not using the remember-me cookies because I have had issues working with them while using flask-kvsession given that I'm having the later store sessions in a redis store as well.

  • #14 Miguel Grinberg said

    @Cheick B: you need to disable session security in Flask-Login, so that the two do not collide. Other than that I think you should be fine.

  • #15 prathamesh chandurkar said

    Miguel You are doing really great job ,thank you for such great post

  • #16 Sutherlander said

    Hi Miguel,

    Like everyone, thanks for sharing your knowledge!

    I have a question about the use session cookies, using Flask-HTTPAuth and Basic authentication I get no session cookies in the response, but using Digest I do:

    GET /print_elite HTTP/1.0
    Authorization: Digest username="cortex", realm="Authentication Required", nonce="759cacd38ce7309e873757c515d6cc53", uri="/print_elite", response="c87e70000f238750e39eddf5de729ec7", opaque="58f40fdb9c7e3ea9d653e31f2702b50d"
    User-Agent: curl/7.29.0
    Host: 127.0.0.1:5003
    Accept: /
    Cookie: session=.eJyrVkosLcmIz8vPS05VsqpWUkhSslLyqwo1jTRyy4py8TWJNAo19q1yNI0KSQfSyYZ-Rr6GviFh2X65fll-Ib62SrU6ECPyCxILS5HMCMnI9XNxyo1y9zSNrErJ8TUKy_APDzTyC_HL8a1yy_XNSi73zfU09HUPBJpRCwCO7yuU.DdZy9Q.oK0FgJZRhE02_K2K7YXvmSay900

    I know in Cherrypy, sessions aren't turned on by default, and Digest authentication doesn't include a Session-Cookie in the response...

    Is there a way to not use Sessions/Session-Cookies in Flask? (preferably while using your Flask-HTTPAuth)

    Much appreciated!

  • #17 Miguel Grinberg said

    @Sutherlander: There is a section of the Flask-HTTPAuth documentation that explains why the session is used for digest auth: https://flask-httpauth.readthedocs.io/en/latest/#security-concerns-with-digest-authentication.

  • #18 Sutherlander said

    @Miguel: Thanks for your response.

    Yes, I read this;
    https://flask-httpauth.readthedocs.io/en/latest/#security-concerns-with-digest-authentication
    a few times (and played around with server side sessions)..
    What I now gather (reading it once again!) is that Flask-HTTPAuth is designed to use session cookies by default to send the challenge data, so obviously turning off sessions/session-cookies won't do (and even server side sessions send an ID of sorts via the session cookie).. I would need to send the challenge data via the authentication header by defining new the nonce and opaque functions, via the provided decorators.
    (something Cherrypy must do with their inbuilt digest authentication (as sessions are turned off by default))

    Is this correct?
    I guess using the header would mean authenticating on each request.
    (which is fine, I'm writing an API as a program's communications layer, and just wanted authentication security over both HTTP & HTTPS (any advice welcome!))

    Thanks again!

  • #19 Miguel Grinberg said

    @Sutherlander: No, you are incorrect. Flask-HTTPAuth does not use sessions to pass the challenges to the client, those go through the appropriate headers according to the HTTP specification. The extension needs to save the generated challenges so that they can be compared against the responses submitted by the client, so the session is used for that storage. Server-side sessions is the easiest mechanism, because that keeps the challenges on the server. But as I said before, you can override the challenge storage mechanism and do whatever you like. For example, you can save the challenges to your database, and then the session would not be needed at all.

  • #20 Sutherland said

    @Miguel: Ok, yes that makes sense; as https://tools.ietf.org/html/rfc2617 doesn't mention anything about sessions in relation to how digest authentication works.. (thanks for straightening me out)

    I guess I was just trying to reconcile Flask, Flask-HTTPAuth, and the seemingly non-trival exercise of removing the use of Sessions and/or the inclusion of Session-Cookie in the server response...

    I can't find any examples/detailed-guidance.. So I guess I'll have to delve deeper, and do a lot more reading, as just being a user of Flask obviously isn't enough >.<
    (If I figure it out.. I'll be the first one to post it on the internet lol)

    Apologies, and thanks again!

  • #21 Miguel Grinberg said

    @Sutherland: the problem is that the digest authentication needs some state to be saved between requests. A server-side session is the ideal place to write this state, but if you have a database you can replace that with something that does not require cookies to be used.

  • #22 Sutherlander said

    @Miguel:

    Yeah, like I mentioned I had created server side sessions using Flask-Session... But for some reason it was still included the cookie header in the server response..

    I also tried to implement Flask-Login's disabling of session cookies;
    https://flask-login.readthedocs.io/en/latest/#disabling-session-cookie-for-apis
    but I think it may have assumed Flask-Login was being used for authentication because I couldn't get it working (I wanted to use Flask-HTTPAuth)...

    I assume things stored in the session (like this digest state), will automatically end up in the cookie header of the response.. This is what I found confusing; "why would it need to be in the response?" (which you've already answered (that it doesn't))

    Anyways, thank you very much!

  • #23 Miguel Grinberg said

    @Sutherlander: server-side sessions use cookies, that is normal. They use them in a different way than the normal cookie-based sessions, in a server-side session the cookie only contains the session id.

  • #24 WAY2Secure said

    Hi,
    I have added following settings -

    app.config.update(
    SESSION_COOKIE_SECURE=True,
    REMEMBER_COOKIE_SECURE = True,
    SESSION_COOKIE_HTTPONLY=True,
    REMEMBER_COOKIE_HTTPONLY = True,
    SESSION_COOKIE_SAMESITE='Lax',
    PERMANENT_SESSION_LIFETIME=600,
    )

    session cookie has 'HTTPOnly' and secure flag both as True
    Remember me cookie has httponly as false and secure flag at True
    sessionid cookie has httponly as True and secure flag as false.

    How can i make httponly as well as secure flag true for all three?

    Also, currently I have these settings in init.py file, will it be better if I move them to config file.

  • #25 Miguel Grinberg said

    @WAY2Secure: difficult to know without seeing the entire app. It could be a bug in your application, or I guess a bug in Flask-Login. Move the settings to config.py, it's always a good idea to keep all the configuration in the same place.

Leave a Comment