flask-paranoid

Flask-Paranoid is a simple extension for the Flask microframework that protects the application against certain attacks in which the user session cookie is stolen and then used by the attacker.

How Does It Work?

When a client connects to the application for the first time, a token that represents certain characteristics of this client is generated and stored. In succesive requests sent by this client, this token is regenerated and compared against the stored one. If the tokens are different, it is assumed that the client is sending requests from a different environment than the one in which the session was originally created, so in this case the session is destroyed and the request rejected as a preventive measure.

By default, the token is generated from the IP address and the user agent of the client. This means that if a user session cookie is stolen and then used from a different location or from a different browser, the generated token will change, and that will block this “different” request.

The idea is based on the strong session protection feature of Flask-Login, but generalized so that it can also be used outside of the Flask-Login context. The token generation and storage can be customized to fit different types of applications.

Quick Start

Here is a simple application that uses Flask-Paranoid to protect the user session:

from flask import Flask
from flask_paranoid import Paranoid

app = Flask(__name__)
app.config['SECRET_KEY'] = 'top-secret!'

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

@app.route('/')
def index():
    return render_template('index.html')

In this example, the paranoid token computed on the initial request from a client will be stored in the user session. If a subsequent request generates a different token, the session will be cleared and then a redirect to the root URL will be made, forcing the potential attacker to log in again.

Configuration

The only configuration is what determines the action that is taken when an invalid session is detected, after the session is cleared. The default is to return a 401 error back to the client.

To redirect to a given URL, set paranoid.redirect_view to the desired location:

paranoid.redirect_view = '/login'

Absolute URLs are also supported:

paranoid.redirect_view = 'https://www.google.com'

To redirect to a registered Flask route, set paranoid.redirect_view to the desired endpoint name:

paranoid.redirect_view = 'index'

To redirect to a route inside a blueprint, use the same syntax used by the url_for() function:

paranoid.redirect_view = 'auth.login'

To have the extension invoke a callback function that generates the response, use the paranoid.on_invalid_session decorator. The function should return a valid Flask response:

@paranoid.on_invalid_session
def invalid_session():
    return 'please login', 401

Using with Flask-Login

Since this extension overrides the similar feature in Flask-Login, it is recommended that when using both extensions in an application, session protection is disabled in Flask-Login:

login_manager.session_protection = None

Flask-Paranoid detects if Flask-Login is being used, and in that case clears the “remember me” cookie in addition to the user session when an invalid session is detected.

Customization

Flask-Paranoid allows applications to customize its inner workings through the use of subclasses.

Changing the Token Generation Algorithm

The default implementation creates a SHA256 hash of the client IP address and user agent.

To use a different token generation algorithm, override the create_token method:

class MyParanoid(Paranoid):
    def create_token(self):
        pass

This method is invoked in the context of a request, so it can access the request object to obtain information about the client.

Changing Where the Token is Stored

The default implementation writes the paranoid token to session['_paranoid_token'].

If a different storage mechanism is desired, override the write_token_to_session() and get_token_from_session() methods:

class MyParanoid(Paranoid):
    def write_token_to_session(self, token):
        pass

    def get_token_from_session(self):
        pass

A tricky use case is when this extension is used with an API project that does not use the user session and instead provides authentication tokens to clients. In such a case, the application’s token generation function can be enhanced to include the paranoid token. For example:

def get_token(username, password):
    if validate_user(username, password):
        return encode_jwt(claims={'username': username,
                                  'paranoid_token': paranoid.create_token()})

And then the token storage methods can be overriden as follows:

class MyParanoid(Paranoid):
    def write_token_to_session(self, token):
        # nothing to do here, the paranoid token is inserted in the JWT
        # by the application
        pass

    def get_token_from_session(self):
        claims = decode_jwt(request.headers['X-API-TOKEN'])
        if 'paranoid_token' not in claims:
            abort(401)
        return claims['paranoid_token']

In this example, the encode_jwt and decode_jwt are application functions that work with JWT tokens. The client authenticates by passing the JWT token in the X-API-TOKEN header.

Using a Different Session Cleanup Mechanism

By default, this extension empties the contents of the user session, and if Flask-Login is used, also deletes its “remember me” cookie.

To change or replace the session cleanup algorithm, override the clear_session() method:

class MyParanoid(Paranoid):
    def clear_session(self, response):
        pass

The response argument passed to this method is the response object that will be returned to the client after the session has been cleaned up. Any cookies that need to be deleted or modified must be included in this response object.