2016-04-04T17:57:45Z

How Secure Is The Flask User Session?

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.

20 comments

  • #1 Usman Ehtesham Gul said 2016-04-05T22:06:02Z

    What is the best way to store the secret key in production servers? I store them as an environment variable? Any recommendations for something better?

  • #2 Miguel Grinberg said 2016-04-06T19:35:49Z

    @Usman: there are lots of ways to store secrets. You can store them encrypted in your database (though you then need a place to store the encryption key...), you can use a secrets database such as Vault, if you run on AWS you can store them in a protected S3 bucket, there's really many places besides the environment. But at the end of the day, your server needs to have a way to obtain these secrets, so in my opinion, it makes more sense to put effort in protecting the server against unauthorized access than finding clever ways to store your secrets.

  • #3 Mike said 2016-04-23T17:31:01Z

    I really enjoyed the article Miguel! I don't think i've ever used the "vanilla" flask sessions. I particularly like using redis which is very easy to implement and add to a configuration management task (like ansible). This flask snippet has worked wonders for me: http://flask.pocoo.org/snippets/75/.

  • #4 Abhay Godbole said 2016-05-20T16:50:41Z

    Hi Miguel, I have just started with Python and Flask and you are my first Mentor. I have watched your Pycon videos and reading your excellent Book. I am working on a product in a financial domain where we would be using IBM Watson services using Python and Flask. This article is very good, but I need to read it again when I get more control on Flask. My question is not related to this article, hope it is ok to ask here. I want to handle multiple forms in a single view. I have one page where I have 3 Tabs and each will have different forms. Currently I am having one route mapped to html in which I have this 3 Tabs. It looks bit tricky to me. Please give me some inputs to handle this scenario. Please get back as per your convenience. Thanks & Regards Abhay

  • #5 Miguel Grinberg said 2016-05-22T00:38:06Z

    @abhay: this will give you some ideas: http://stackoverflow.com/questions/21949452/wtforms-two-forms-on-the-same-page

  • #6 Abhay said 2016-05-22T06:49:59Z

    Thanks Miguel... This will help me. I will try on this, if I face any issue, will get back

  • #7 Iron Fist said 2016-06-19T02:05:06Z

    Interesting article, I always had in my mind the question about how secure are the session cookies since in Flask'docs[1] they mentioned: " What this means is that the user could look at the contents of your cookie but not modify it, unless they know the secret key used for signing." <a href="http://flask.pocoo.org/docs/0.11/quickstart/#sessions">[1]</a>

  • #8 Aris said 2016-08-12T04:31:24Z

    Hello Miguel, I was a little bit confused, in the example code, what does 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!" actually mean? Thanks in advance.

  • #9 Miguel Grinberg said 2016-08-16T19:47:14Z

    @Aris: that's just so that it is clear that the implementation is completely wrong. I don't want you to see that code and think it is okay to copy/paste it. The idea is that if you have any information you don't want your clients to discover, then you cannot put it in the user session, since that is unencrypted.

  • #10 senaps said 2016-11-28T21:43:00Z

    so, based on what we see, i can save the user state(loged_in) and they can see it, but can't change it anyway... am i safe to do that?

  • #11 Miguel Grinberg said 2016-11-28T22:24:26Z

    @senaps: Correct. You can save a user id or any other public way to identify a user and a logged in state. These can be seen by anyone, but they cannot be changed (as long as you keep your Flask secret key secret).

  • #12 QiTian said 2017-01-23T13:11:35Z

    Hi, miguel, First I am grateful for your guide about flask web, But I have some questions about flask user session maintain! I have known that we created a session object with unique sessionID to response to client when a user first logged, and then when user request others' they will request with a cookie with that ID, so server can find the session object by that ID, which will denote the user have logged! But this is one user situation, I find most blogs doesn't say if there are many users to manage, we would create many sessions in memory to every user?. I think so! But when I lookup flask-login source code, I can't find a session collections to maintain session for every user? ```python def login_user(user, remember=False, force=False, fresh=True): ''' Logs a user in. You should pass the actual user object to this. If the user's `is_active` property is ``False``, they will not be logged in unless `force` is ``True``. This will return ``True`` if the log in attempt succeeds, and ``False`` if it fails (i.e. because the user is inactive). :param user: The user object to log in. :type user: object :param remember: Whether to remember the user after their session expires. Defaults to ``False``. :type remember: bool :param force: If the user is inactive, setting this to ``True`` will log them in regardless. Defaults to ``False``. :type force: bool :param fresh: setting this to ``False`` will log in the user with a session marked as not "fresh". Defaults to ``True``. :type fresh: bool ''' if not force and not user.is_active: return False user_id = getattr(user, current_app.login_manager.id_attribute)() session['user_id'] = user_id session['_fresh'] = fresh session['_id'] = _create_identifier() if remember: session['remember'] = 'set' _request_ctx_stack.top.user = user user_logged_in.send(current_app._get_current_object(), user=_get_user()) return True ``` There is one session to keep the user, but what if another user come? ```python # -*- coding: utf-8 -*- """ flask.globals ~~~~~~~~~~~~~ Defines all the global objects that are proxies to the current active context. :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ from functools import partial from werkzeug.local import LocalStack, LocalProxy def _lookup_req_object(name): top = _request_ctx_stack.top if top is None: raise RuntimeError('working outside of request context') return getattr(top, name) def _lookup_app_object(name): top = _app_ctx_stack.top if top is None: raise RuntimeError('working outside of application context') return getattr(top, name) def _find_app(): top = _app_ctx_stack.top if top is None: raise RuntimeError('working outside of application context') return top.app # context locals _request_ctx_stack = LocalStack() _app_ctx_stack = LocalStack() current_app = LocalProxy(_find_app) request = LocalProxy(partial(_lookup_req_object, 'request')) session = LocalProxy(partial(_lookup_req_object, 'session')) g = LocalProxy(partial(_lookup_app_object, 'g')) ``` I find session is an global variable, and is an localstack(), but I still don't konw how does it works? ```python class Local(object): __slots__ = ('__storage__', '__ident_func__') def __init__(self): object.__setattr__(self, '__storage__', {}) object.__setattr__(self, '__ident_func__', get_ident) def __iter__(self): return iter(self.__storage__.items()) def __call__(self, proxy): """Create a proxy for a name.""" return LocalProxy(self, proxy) def __release_local__(self): self.__storage__.pop(self.__ident_func__(), None) def __getattr__(self, name): try: return self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name) def __setattr__(self, name, value): ident = self.__ident_func__() storage = self.__storage__ try: storage[ident][name] = value except KeyError: storage[ident] = {name: value} def __delattr__(self, name): try: del self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name) ``` Many people say it will use another thread id to identify, storage[ident][name] = value , but i disable threading, it works well for multi-users? I just find it use current_user variable to identify current user, but current_user is so magic! It doesn't maintain user session collection but just one current_user to solve the problem! I don't know how it works? ```python def login_required(func): ''' If you decorate a view with this, it will ensure that the current user is logged in and authenticated before calling the actual view. (If they are not, it calls the :attr:`LoginManager.unauthorized` callback.) For example:: @app.route('/post') @login_required def post(): pass If there are only certain times you need to require that your user is logged in, you can do so with:: if not current_user.is_authenticated: return current_app.login_manager.unauthorized() ...which is essentially the code that this function adds to your views. It can be convenient to globally turn off authentication when unit testing. To enable this, if the application configuration variable `LOGIN_DISABLED` is set to `True`, this decorator will be ignored. .. Note :: Per `W3 guidelines for CORS preflight requests <http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_, HTTP ``OPTIONS`` requests are exempt from login checks. :param func: The view function to decorate. :type func: function ''' @wraps(func) def decorated_view(*args, **kwargs): if request.method in EXEMPT_METHODS: return func(*args, **kwargs) elif current_app.login_manager._login_disabled: return func(*args, **kwargs) elif not current_user.is_authenticated: return current_app.login_manager.unauthorized() return func(*args, **kwargs) return decorated_view ``` So where is process of comparing current user sessionID from cookie with session collection mantained by server? Anybody can help me? I think that flask-session must depend on cookie enabled !

  • #13 Miguel Grinberg said 2017-01-23T15:55:59Z

    @QiTian: Have you watched the video? Sessions in Flask do not work like you describe at all, the entire session payload is in the cookie. There is no session ID, and the server does not store any session data. You are describing a different session implementation, not what Flask does.

  • #14 Flipper said 2017-02-16T00:12:28Z

    Really useful post which might save a few people's bacon. What I'm not sure of is why my Flask session cookie doesn't seem to be subject to a base64 decode to JSON, unlike yours. Here's the raw cookie from Chrome dev tools: Set-Cookie:session=.eJy1lG1v0zAQx7-K5VcgmQla8aC8ZHRSxWihqxjSVFWuc0mMHDv4bMKo-t13TtO1GZX2ine5p__vfL54y40rS8jX2vIs-AiC14VcIygPgWf83fdv4-nn5dXix-h2dH31lguunC10ybNtKnVUxlu92ejwd0zBYCQ5bqcf6TuHQkYT1vCn0f6e3OPXyatRbgysCw0mR57dcQ8YvFZBO4uUgC56BR0oB-Qrwa2sIanuMSmlcj6se3f76D4Ae4XsbnsoXUAM4LEXTY7lInV73yRDNWFJX1k6ndGUKA15l5Wr0VnW17LrkPPd6kgZtH3Cmjn25EQ9czZfXP4LfTMeUGfz2WSA2U_hRP_GKQX-RHaU3cwvzyi__zBQnvwGG5B6MzJoW7LgWKiAYUOzZK5g2Ole8J04olJsSPo6fxbUVaFgrQ4Vk6xwKiKjQW4c2R0u4WHfDoFpoRB-RbAKBJM279pqnTf5q1bnwDYRtQXscrtyvGBTq0ykwWRMVbJu0qAr3fSigtXyp_MMdWkJRXZSrB0GpuskIG14VBXMQClNB_ZQRpqO8_ekENKtC4ZkaugOcCy20LIAskYqlmVMYX-Ad7dHXVkLZrCejUPUafX3-4n_ZUHFU0n2IofavRwof5p8OXONz6ofpHarnTj386eXY_86RA_56XsiI92ADVrJMAyATY8BuQppEHYPqd2Ntg.C4XqAQ.hHtRENE7MgOhAoZ2DQsZxGY9u88; HttpOnly; Path=/ Taking just the section from ".eJy..." to "..Ntg" as you've done, with "===" padding, base64.urlsafe_b64decode() returns what looks like an opaque bytes object: b'x\x9c\xb5\x94mo\xd30\x10\xc7\xbf\x8a\xe5W \x99\tZ\xf1\xa0\xbcdtR ..... This is with Python 3.5.2 / Flask 0.11, and the app is doing nothing clever, just setting app.secret_key = os.urandom(24) as suggested in the Flask docs, and using the default session cookie implementation. Any idea what's different from your case? I'd like to believe that some Flask/Werkzeug update has added cookie encryption, but that's probably wishful thinking. (Feel free to post the decoded session cookie if it works for you - it's only a dev/qa system which means nothing to the outside world)

  • #15 Miguel Grinberg said 2017-02-16T03:38:31Z

    @Flipper: The initial period should not be included, it just indicates that the payload is compressed. See the second part of the youtube video, there I show how to decompress the session.

  • #16 Flipper said 2017-02-16T10:15:24Z

    Ah right, it's compressed. I was following along with your video and stopped when I found that my cookie didn't behave the same as in the video. That'll teach me to watch to the end. Thank you again

  • #17 Flipper said 2017-02-17T15:05:58Z

    FYI I have created a simple drop-in alternative to the default Flask session cookie implementation, which encrypts the session data with AES-256 using the pycryptodome package: https://github.com/SaintFlipper/EncryptedSession

  • #18 Miguel Grinberg said 2017-02-18T02:10:20Z

    @Flipper: Nice project! I would only recommend that you use the Flask config object for the crypto key, defaulting to the secret key if the application does not provide a specific key for this purpose.

  • #19 khan said 2017-04-16T10:03:02Z

    Do you mean login using session is not secure . below code is copied from this tutorial https://code.tutsplus.com/tutorials/intro-to-flask-signing-in-and-out--net-29982 @app.route('/signin', methods=['GET', 'POST']) def signin(): form = SigninForm() if request.method == 'POST': if form.validate() == False: return render_template('signin.html', form=form) else: session['email'] = form.email.data return redirect(url_for('profile')) elif request.method == 'GET': return render_template('signin.html', form=form)

  • #20 Miguel Grinberg said 2017-04-17T03:00:11Z

    @khan: There's nothing wrong with the example you pasted. Only the email address is written to the user session. The client has no way to forge a session with a random email (unless the server's secret key is leaked), so security is not a concern. The only thing is that the email will be stored without encryption on the session. This is usually not a problem.

Leave a Comment