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
  • #1 Usman Ehtesham Gul said

    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

    @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

    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

    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

  • #6 Abhay said

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

  • #7 Iron Fist said

    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."
    [1]

  • #8 Aris said

    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

    @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

    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

    @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

    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?

    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?

    # -*- 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?

    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?

    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

    @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

    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

    @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

    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

    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

    @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

    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

    @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.

  • #21 Thierry Michel said

    Hi Michael,

    Thank you for this very enlightening explanation.

    I am a noob and i apologise in advance if this question is stupid.

    I have actually landed here because i was wondering how Flask prevents CSRF - mainly with oAuth.

    When implementing oAuth the tutorial recommends adding a state variable/token in the session object. As i understand it, the token is sent back by the browser(in the request) and i can check that token against what is in the session.

    If the session is visible(and it is see below from my oAuth test) as explained in your video, does it make sense to save that token in the session or is that exactly the type of data that you are recommending we do not save in the session?

    b'{"Grinberg":"woohoo","oAuthState":"uhasl2aNoT2JktPGzWDuan2DG1UIYY","test":"Thierry test","token":"EAABwyg7arKsBAOzqmBHr3BhTY3gwWRKnHrZCid13T77zEvGlZC4SXNq85XR1BMa1R3hEJKeHMaZBxn5UoMoy83VYVOSaskQKmUIGWoeyMssb4AXm68L8GhGjujjPgJhfm6Odbnwl5PIX9ZAPZBEZBZAo7OGZAFMG4zR3wV7ZBQ6nHnQZDZD","tokenExpires":1509003460.2205567}'

    Thank you again for your work.

  • #22 Miguel Grinberg said

    @Thierry: My interpretation of the solution, is that you have to add a "state" parameter to the payload that goes out to the OAuth provider. This can be anything you want, as long as it is unique for each user, and very hard to guess. The provider will send this value back to you on the callback, where it can be verified. The problem is how does the server store this value, so that it can have it by the time the callback is invoked. The safest place to store it is in a server-side database. Also safe would be to store it in the user session if you use server-side session storage. Storing it in the user session when using the default signed cookie sessions from Flask is less safe, but it isn't very unsafe, in any case. Consider that the session cookie for your application will not be readable by the attacker's application, first because the session cookie is "httpOnly", and second, because the attacker's site has no read access to the cookies of other sites including yours. So using the session cookie is still okay, unless you are worried about compromised browsers, etc. Hope this helps!

  • #23 Deepak said

    Hi Miguel,

    What I have to do to initialize Sessions in my flask app. The steps provided by flask doc doesn't work.

    sess = Session()
    sess.init_app(app)

    Error:
    Traceback (most recent call last):
    File "./wsgi.py", line 2, in <module>
    from manage import app as application
    File "./manage.py", line 12, in <module>
    app = create_app(os.getenv('FLASK_CONFIG') or 'development') # pylint: disable=invalid-name
    File "./app/init.py", line 34, in create_app
    sess.init_app(app)
    AttributeError: 'SecureCookieSession' object has no attribute 'init_app'

    This gives me init_app absent in the session class which is True. Without session's I get following error. I didn't see this error when I hosted server in a vm and loaded from another vm in my PC. But when I moved my server to aws, I started seeing it.

    File "./app/oauth/views.py", line 69, in oauth_authorize
    session['post_id'] = post_id
    File "/home/bitnami/flask/local/lib/python2.7/site-packages/werkzeug/local.py", line 346, in setitem
    self._get_current_object()[key] = value
    File "/home/bitnami/flask/local/lib/python2.7/site-packages/flask/sessions.py", line 126, in _fail
    raise RuntimeError('The session is unavailable because no secret '

    Do you have any idea regarding this?

    Thanks,
    Deepak

  • #24 Miguel Grinberg said

    @Deepak: Where in the Flask docs did you see Session.init_app() mentioned? The Flask session object does not require initialization, the only thing you need to do is define the SECRET_KEY variable in your configuration.

  • #25 Ali said

    This is a great article. Just one question. How secure are server side variables using the Flask-Session library? I’d like to store user passwords as I need to reuse these across flask requests.

Leave a Comment