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
  • #26 urbainy said

    Hi, Miguel,
    I followed your book to enter the web developing field. Really thanks for you!
    As my understanding, your book mainly describes two scenarios: a) Fully server side rendering technology based on Jinja. b) Restful API for a smart client, such as android app.
    Both of the two scenarios are clear for the development of the server logic. Your book is enough for me now.
    But now I face another scenario for me (maybe it is not special thing for you) , browser <-> vuejs fontend <-> flask restful api.
    I totally confused to do the security relative things even though I read plenty articles till now. I have selected flask-jwt-extended as the basic authentication utility. It supports token embedding cookie with httponly setting. So I understand this technology immune to XSS attack. Now I don't know how to defense CSRF attack in my scenario. I don't know if I have to use flask-seasurf extension. All of the post forms will be generated by vuejs, so no flask-wtf will be used. And because there is no page will be rendered by flask, so how to insert the csrf-token to the page which is controlled by vuejs.

  • #27 Miguel Grinberg said

    @urbainy: You can return the CSRF token in a cookie, which then the client needs to copy into a custom header. This method is called the double submit cookie, it is documented here: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Double_Submit_Cookie.

  • #28 Nate said

    Hi @Miguel

    I installed Flask-Paranoid to cover a potential vulnerability in my software (which it does), however the downside is that when toggling between web- and responsive-mode the refresh logs the user out. I can see that this is a good thing from the module's perspective, however my potential client-base are obsessed with UAT, and will often toggle between different platform simulations to compare behaviour.

    It's probably a really simple thing, but can you suggest a way that I might allow testers to disable Paranoid while testing? (I'm using an app factory and initialising paranoid with "paranoid.init_app(app)" )

    I considered either a per-user basis, or environment-wide (obviously I'd also be communicating that this option will not be available in a Prod environment, and should not be used when running penetration testing)... but to be honest, I can't find anything about temporary disabling modules...

    Cheers

  • #29 Miguel Grinberg said

    @Nate: can you add a setting in flask.config? Depending on the value of the setting you initialize flask-paranoid or not.

  • #30 Sander said

    Great summary Miguel. Question on session IDs that I can’t seem to find in the flask-login documentation. Are these automatically regenerated on login / logout (for added security)?

  • #31 Miguel Grinberg said

    @Sander: I don't understand. What do you want regenerated on login/logout?

  • #32 Seamus said

    Miguel, thank you for your great blog. I've got a large web app running without any problems - except for forms only working on the first browser I use.

    Logging a user in seems to lock that user into the browser that I used. If I login and logout a user, and switch to another browser, the login page will simply refresh (without showing any error).

    This behaviour also happens when, after logging in and out with a user, I open a new private tab and attempt to send a reset password email. The form simply resets (page refreshes) without any error messages. No email is sent. If I return to the original browser tab, and submit the reset password form, it works fine.

    I am struggling to troubleshoot this odd behaviour - could it be related to flask wtf, CSRF tags, or my secret key? I have 4 workers through Gunicorn, but they should share the same secret key.

  • #33 Miguel Grinberg said

    @Seamus: Is there any chance that your form rendering is incorrect and somehow misses to show the error? My guess is that the problem is related to bad or missing CSRF, but I don't see how that could be related to first vs second browser.

  • #34 YM said

    Thank you Miguel for the magnificent tutorials.
    I am perplexed with something when I am securing my cookie. I have not changed the permanent session flag so that means that my session is volatile by default with expiry set to session.
    However, if I set permanent_session_lifetime to 1 minute, the session expires after exactly 1 minute although when I inspect the cookie in the browser it says "session" for expiry.
    I have no idea how the browser or my flask app know that this session needs to expire. I reviewed flask and flask-wtf and flask-login source code but could not reach a conclusion.

  • #35 Miguel Grinberg said

    @YM: The permanent session lifetime setting applies to the session itself, not to the cookie where the session is stored. The documentation for this setting explains this:
    "Flask’s default cookie implementation validates that the cryptographic signature is not older than this value."

  • #36 Joe said

    It's great, not to worry about CSRF, as the Flask-Paranoid already handled it automatically. Its been a few years since this is posted, wondering if this still relevant, and why not used it in your "Flasky" project?

  • #37 Miguel Grinberg said

    @Joe: Flask-Paranoid has nothing to do with CSRF attacks. You have it confused with Flask-WTF which automatically protects web forms against CSRF. For any views that are not web forms you can manually enable CSRF using the CsrfProtect class that also comes with Flask-WTF.

  • #38 Brandon said

    Hi Miguel,

    This article is great! I'm wondering how to securely implement a remember_me cookie into this setup. As soon as I add the login_manager.session_protection = strong to Flask login I find I have to log in every time I close my browser. The only way I've managed to get this to work is leaving session_protection on basic. Do you happen to have any suggestions for securing the cookies while keeping the remember_me functionality?

    Thanks so much

    • Brandon
  • #39 Miguel Grinberg said

    @Brandon: I speak about this in the article. Have you read all of it? See the part that talks about the Flask-Paranoid extension.

Leave a Comment