How Secure Is The Flask User Session?

Posted by
on under

Many times I hear people say that user sessions in Flask are encrypted, so it is safe to write private information in them. Sadly, this is a misconception that can have catastrophic consequences for your applications and, most importantly, for your users. Don't believe me? Below you can watch me decode a Flask user session in just a few seconds, without needing the application's secret key that was used to encode it.

So never, ever store secrets in a Flask session cookie. You can, however, store information that is not a secret, but needs to be stored securely. I hope the video makes it clear that while the data in the user session is easily accessible, there is very good protection against tampering, as long as the server's secret key is not compromised.

The Example Application

Want to play with sessions using the "Guess the Number" application I created for the video demonstration? No problem, below you can find the code and templates. As you know, I always put my examples on github, but since this is an example of how not to do things, I am reluctant to have this in there, where people can see it out of context and misunderstand it. So I'm sorry, but for this example, you are going to have to copy and paste the four files.

The application is in file guess.py:

import os
import random
from flask import Flask, session, redirect, url_for, request, render_template

app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') or \
    'e5ac358c-f0bf-11e5-9e39-d3b532c10a28'

@app.route('/')
def index():
    # the "answer" value cannot be stored in the user session as done below
    # since the session is sent to the client in a cookie that is not encrypted!
    session['answer'] = random.randint(1, 10)
    session['try_number'] = 1
    return redirect(url_for('guess'))

@app.route('/guess')
def guess():
    guess = int(request.args['guess']) if 'guess' in request.args else None
    if request.args.get('guess'):
        if guess == session['answer']:
            return render_template('win.html')
        else:
            session['try_number'] += 1
            if session['try_number'] > 3:
                return render_template('lose.html', guess=guess)
    return render_template('guess.html', try_number=session['try_number'],
                           guess=guess)

if __name__ == '__main__':
    app.run()

There are also three template files that you will need to store in a templates subdirectory. Template number one is called guess.html:

<html>
    <head>
        <title>Guess the number!</title>
    </head>
    <body>
        <h1>Guess the number!</h1>
        {% if try_number == 1 %}
        <p>I thought of a number from 1 to 10. Can you guess it?</p>
        {% else %}
        <p>Sorry, {{ guess }} is incorrect. Try again!</p>
        {% endif %}
        <form action="">
            Try #{{ try_number }}: <input type="text" name="guess">
            <input type="submit">
        </form>
    </body>
</html>

Template number two is win.html:

<html>
    <head>
        <title>Guess the number: You win!</title>
    </head>
    <body>
        <h1>Guess the number!</h1>
        <p>Congratulations, {{ session['answer'] }} is the correct number.</p>
        <p><a href="{{ url_for('index') }}">Play again</a></p>
    </body>
</html>

And the last template is lose.html:

<html>
    <head>
        <title>Guess the number: You lose!</title>
    </head>
    <body>
        <h1>Guess the number!</h1>
        <p>Sorry, {{ guess }} is incorrect. My guess was {{ session['answer'] }}.</p>
        <p><a href="{{ url_for('index') }}">Play again</a></p>
    </body>
</html>

If Sessions Aren't Secure Enough, Then What Is?

Now you know that the Flask user session is not the right place to store sensitive information. But what do you do if you have secrets that need to be stored, and using a database seems overkill?

One very good solution is to use a different user session implementation. Flask uses cookie based sessions by default, but there is support for custom sessions that store data in other places. In particular, the Flask-Session extension is very interesting, as it stores the user session data in the server, giving you a variety of storage options such as plain files, Redis, relational databases, etc. When the session data is stored in the server you can be sure that any data that you write to it is as secure as your server.

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!

50 comments
  • #26 Miguel Grinberg said

    Server side sessions come in many flavors, so the security of the data depends on what session storage you use. I recommend that you do not store user passwords if possible. You can store a password hash to make it more secure, or if you really need to have the actual password, then encrypt it and store the encryption key somewhere else.

  • #27 Will said

    Hi, I'm fairly new to Flask, but I like it so far. I am using the method described in the flask.pocoo.org quickstart guide:

    from flask import Flask, session, redirect, url_for, escape, request

    app = Flask(name)

    @app.route('/')
    def index():
    if 'username' in session:
    return 'Logged in as %s' % escape(session['username'])
    return 'You are not logged in'

    Anyways, this works great, but I am concerned with the following. If I login, I can copy the value of the cookie (Everything, including the periods and the characters around them), bring that value to another computer, and insert them into a newly created cookie on that computer. Then I am logged in.
    1) Is this a legit use case, or is this hard for attackers to do? Obviously not copy and paste, but to get access to that cookie value...
    2) Do you know of any good ways prevent this?

    Thanks!

  • #28 Miguel Grinberg said

    @Will: you are correct, if you take a session cookie from machine A and put it machine B, then machine B will appear as logged in. Browsers have options to try to prevent cookies from being stolen, so as long as you understand them and use them appropriately, your risk decreases considerably. I have written about the topic in this blog: https://blog.miguelgrinberg.com/post/cookie-security-for-flask-applications, and have also created a Flask extension called Flask-Paranoid that helps protect your user session cookies.

  • #29 Tim said

    Any idea on why Flask stores the entire session payload in cookies? Everyone else just stores a unique key to lookup the relevant information that's stored on the server and never sends the actual session contents to the server. This seems to be a good way to accidentally expose people's secrets since Flask's behavior is completely opposite of what's normally expected, isn't it?

  • #30 Miguel Grinberg said

    @Tim: I believe you are wrong, Flask isn't the only framework that does this, and it is certainly not the first. For example, Django has them too. Cookie based user sessions are convenient and highly scalable, as they require no server-side storage, so they are appropriate for a lot of applications. The Flask-Session extension provides additional session backends, so you can work with user session that are stored in the server if you prefer that model.

  • #31 Pritish Sehzpaul said

    Hi Miguel,
    So, I followed the steps and decrypted the cookie but could find nothing readable. It was encrypted and seemed pretty random with the value in hexadecimals.
    I believe you should update your article or archive this one so that we could mark it as a learning phase for Flask's history.

    Cheers :)

  • #32 Miguel Grinberg said

    @Pritish: this is still valid, as far as I know. Maybe you've got a compressed cookie. This is covered in the video.

  • #33 Fernando A. said

    Hello, I was reading this article and seems fantastic. I have an issue in my code since I change to Flask-Session. When I try to perform a request through a form I receive an error which says:
    {'csrf_token': ['The CSRF session token is missing.']}
    and no, the token is there in the HTML form. And when I try to check in the cookie session I have a value that I cannot decrypt as you show in your video, because is generated by Flask-Session.
    What is the issue with that??

  • #34 Miguel Grinberg said

    @Fernando: are you sure Flask-Session is working? If the session isn't being written correctly then the CSRF token will change every time. That's the only theory I can offer, your user session isn't being written so your session is reset with every request.

  • #35 Fernando A. said

    @Miguel thanks you very much for answer me, let me explain well. When I run the app in my computer work perfectly every time, and I can have many requests at the same time form different computers. But when I try to run in my Rasberry Pi using UWSGI server I can't handle multiples request from multiples computers. So make me wonders if this is the server's fault?

  • #36 Fernando A. said

    And another thing, I was looking now your conference "Miguel Grinberg - Microservices with Python and Flask - PyCon 2017" and one thing isn't clear for me. What is best for Tokens Flask-Sessions or JWT?? or is possible to use both??
    Because when I use JWT is very easy to decrypt the information using this video tutorial, that means that I have to provide the info already encrypted in the token??

    **Currently I'm using Flask-Session with SQLAlchemy
    Thanks a lot and sorry for so many questions

  • #37 Miguel Grinberg said

    @Fernando: How many worker process are you running with uWSGI? It's likely a configuration problem. Set it to use four workers and you'll be able to handle four requests concurrently.

    Can't really say what's best on server or client-side sessions, it depends on each case. Sessions aren't supposed to hold sensitive information, so they normally don't need encryption. If you are storing sensitive data you should use server-side sessions.

  • #38 Fernando A. said

    Thank you so much for the help, I set uWSGI server with 4 workers and is working very well.

  • #39 Fernando A. said

    Talking about Microservices, there is one thing that isn't clear for me. If each microservices has his own database, and for example, one microservice is down, his database's information isn't' available no?? And what happens if I need that information, how could I have it??
    Or if the microservice is killed, the database with the data is killed too??

  • #40 Miguel Grinberg said

    @Fernando: you normally run more than one instance of each microservice. If a microservice dies, another instance takes over until the dead one can be restarted.

  • #41 Edon said

    Hello, coming back to this, I've found out that if you use nginx reverse-proxy, I'm unable to decode base64 cookies, it appears to be encrypted.
    Anyone familiar with this?

  • #42 Miguel Grinberg said

    @Edon: Did you see the part about compressed cookies? My guess is that you now have one that is compressed, not encrypted.

  • #43 mohamed said

    Hi, I would just like to know, is it the same case today that flask sessions are not secure? I noticed that this post is 4 years old.

  • #44 Miguel Grinberg said

    @mohamed: I think you did not understand what I was trying to convey in this post. The Flask sessions are 100% secure, they were secure 4 years ago and continue to be secure today. What they are not is encrypted, which has nothing to do with being secure or not.

  • #45 Ben said

    Hey, Miguel,
    You do a great job explaining why everyone seems to say that flask sessions are not secure. Quick question, though, would it be 'private' to encrypt a value you need in a session but then decrypt it when you access the session. For example, if you wanted to store the user's social security number in the session but you want to keep it 'private', could you encrypt it in your code using a key and then just decrypt it when your program reads the session variable. Yes, it would be sent to the client but only the encrypted version would be sent there which, if your encryption is strong enough, wouldn't be decodable anyway.

    Is there any reason why that wouldn't work?

    Once again, really appreciate this post!
    Ben

  • #46 Miguel Grinberg said

    @Ben: encrypted data is often hacked. By putting an encrypted variable in the user session you are giving access to your data. If the attacker figures out or steals your encryption key, then all bets are lost. You have to be really sure that you know what you are doing in terms of encryption to make your encrypted variables public. If we are talking about very sensitive details such as a SSN, I would never take a risk. Store them in your database, or use server-side sessions.

  • #47 Tungki Reza Prasakti said

    Hey Miguel, I still can't figure out the second dot in the token, you say in the video it was an Expiration date, so how to decode it? I've tried decode as you did in the video but it's seem like on different format

  • #48 Miguel Grinberg said

    @Tungki: the expiration is written as an 8 bytes timestamp integer if I remember correctly.

  • #49 Mohamed El-Hasnaouy said

    Interesting... That brings a question:
    How can I encrypt that data using a custom method of mine before sending it to the browser?
    and of course decrypting it back into plaintext before it enters flask again, Can we set some listeners for that?

  • #50 Miguel Grinberg said

    @Mohamed: If you have data that is private, then it is a very bad idea to send it to the client, encryption or not. The best alternative is to store this data in the server, for example in a database, where it is more protected.

Leave a Comment