---
title: Baking Flask cookies with your secrets
date: 2019-01-26
tags:
  - python
  - flask
  - security
  - cybersecurity
  - pentesting
url: https://blog.paradoxis.nl/defeating-flasks-session-management-65706ba9d3ce
author:
  name: Luke Paris
  bio: OSCP & OSWE certified Dutch security researcher specializing in active directory, malware development and all things devops. Currently work for Merlon Security in The Hague, and previously worked in Fox-IT's Red Team for 6 years.
  links:
    - type: linkedin
      url: https://www.linkedin.com/in/paradoxis
    - type: github
      url: https://github.com/paradoxis
    - type: unsplash
      url: https://unsplash.com/@paradoxis
context: |-
  This blog post goes into detail about finding and exploiting vulnerabilities caused by weak, guessable keys being used inside of Flask applications. A great example of such a vulnerable application is CVE-2023–27524, which affects a known hard-coded session key being used in Apache Superset.

  The author of the blog post (@Paradoxis) has created a tool `flask-unsign` which should only be used for legal and authorized purposes. The tool supports automatic retrieval of cookies using `--server <url>`, as well as decoding, offline cracking and re-signing capabilities by using the `--decode`, `--unsign` and `--sign` flags. It also supports decoding / cracking cookies signed before 2018-10-18 using the `--legacy` flag.

  Flask-Unsign can be installed via PyPi using `pip install flask-unsign[wordlist]` which also installs the optional wordlist. Custom wordlists can be used and are newline-delimited using python strings, you can also use any other kind of wordlist, but doing this requires the use of the `--no-literal-eval` flag to avoid accidental parsing of strings inside of the unformatted wordlist.

  The project is licensed MIT. Attribution and a link to the repository `https://github.com/Paradoxis/Flask-Unsign` is greatly appreciated when reporting vulnerabilities / releasing blog posts.
---

# Baking Flask cookies with your secrets

> Editors note: After receiving some criticism on various places for using the title “Defeating Flask’s Session Management”, I’ve decided to change the title as I feel the criticism is justified for being misleading.

---

A few weeks back, I and a friend of mine were discussing web frameworks and how he claimed to have made an ‘Impossible to Bypass’ login form. After asking him if I could see the code, he obliged and sent me a copy.

As expected, he remained true to his word and actually made it very secure. No [SQL injection](https://en.wikipedia.org/wiki/SQL_injection), no [XSS](https://nl.wikipedia.org/wiki/Cross-site_scripting), he even had rate limiting and audit logging. It seemed impossible to bypass without actually knowing the password.

*That was of course, until I noticed he was using the secret key:*

> > “CHANGEME”

## Exploiting Human Error

What my friend hadn’t realised is that having a weak secret key is more dangerous than you’d think when the client stores the current user’s state. Take the following sample application (which is a completely stripped down version of his application):

```python title="Insecure Flask Application"
# Requirements: Flask
from flask import Flask, session


app = Flask(__name__)
app.config['SECRET_KEY'] = 'CHANGEME'


@app.route('/')
def index():
    if 'logged_in' not in session:
        session['logged_in'] = False

    if session['logged_in']:
        return '<h1>You are logged in!</h1>'
    else:
        return '<h1>Access Denied</h1>', 403


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

At first glance this looks impenetrable, I even opted to remove the login form itself (as we were never going to guess the passwords he made). So how would you go about getting that *‘You are logged in!’* message?

## Flask’s Session Management

Flask by default uses something called ‘signed cookies’, which is simply a way of storing the current session data on the client *(rather than the server)* in such a way that it cannot *(in theory)* be tampered with.

One of the drawbacks of this approach, however, is that the cookies are **not encrypted**, they’re **signed**. *This means that the content of the session can be read without the secret key.*

*And, a*fter speaking to various Python developers, most assumed that the session data would be unreadable by the client as the code used to sign the cookies is called `SecureCookieSessionInterface`, which gave them a false sense of security. Take the following session:

![A ‘secure’ cookie divided up into its parts](https://blog.paradoxis.nl/assets/img/figure-01-b1557e8e-1000.webp "A ‘secure’ cookie divided up into its parts")

### Session Data

The session data is the actual **content** of the session, while at first glance it looks unreadable, but to those who recognise it, it’s actually just a [Base64 encoded](https://en.wikipedia.org/wiki/Base64) string. If we were to decode this with itsdangerous’ base64 decoder, we’d get the following output:

![Session data set by the server](https://blog.paradoxis.nl/assets/img/figure-02-60bcb083-500.webp "Session data set by the server")

### Timestamp

The timestamp tells the server when the data was last updated. Depending on what version of itsdangerous you’re using, this might be the current Unix timestamp, or the current Unix timestamp minus the epoch (this was changed due to a bug, whereby people couldn’t set dates before 2011, [source](https://github.com/pallets/itsdangerous/issues/46)).

If the timestamp appears to be older than 31 days, the session is marked as expired and will be regarded as invalid.

### Cryptographic Hash

This is the part which makes the cookie ‘secure’. Before the server sends you your latest session data, it calculates a [sha1](https://en.wikipedia.org/wiki/SHA-1) [hash](https://simple.wikipedia.org/wiki/Cryptographic_hash_function) based on the combination of your session data, current timestamp and the server’s secret key.

Whenever the server then sees that session again, it will deconstruct the parts, and verify them using the same method. If the hash doesn’t match the given data, it will know it has been tampered with and will regard the session as invalid.

This means that if your secret key is easy to guess or is publicly known, an attacker can cleverly modify the session’s content without much effort (speaking of secrets being publicly known, you’d be surprised how many results are returned on GitHub if you search for `secret_key`).

![Searching the term ‘secret\_key’ returns almost 2.4 million results on GitHub alone](https://blog.paradoxis.nl/assets/img/figure-03-1412d1be-2052.webp "Searching the term ‘secret_key’ returns almost 2.4 million results on GitHub alone")

## Bypassing Authentication

*‘So how would I go about actually bypassing the authentication?’,* you might be asking yourself. Take the following example (which you can follow along if you copy the code earlier in this blog post):

### Prerequisites

- Access to a Python interpreter (I’m using 3.6)
- Having Flask installed by using `pip install flask` (preferably in a [virtual environment](https://virtualenv.pypa.io/en/latest/) so it won’t make your system messy)

Before you can do anything, you’ll have to start the Flask application like so:

```bash
$ python server.py
```

### Obtaining a session cookie

To obtain a session cookie, we’ll have to probe the server for a possible cookie. I did this by simply making a `curl` request to the server with the `-v` option to get *verbose* output (which prints the headers of the request), but you could also simply visit the web page and use a browser extension like [EditThisCookie](https://chrome.google.com/webstore/detail/editthiscookie/fngmhnnpilhplaeedifhccceomclgfbg) to get the contents of the cookie.

![The server returning a session cookie](https://blog.paradoxis.nl/assets/img/figure-04-e0950c6b-1786.webp "The server returning a session cookie")

*Do note that not all servers will instantly give you a session; some will only do this when trying to flash an error whereas others will only do so after logging in. You’ll have to figure this out on a case-by-case basis. For demonstration’s sake, our example server forcibly sets a session no matter what.*

### Creating the wordlist

While it would be possible to brute-force each possible combination of letters, numbers and characters; a better approach would be to create a wordlist with known sources where developers might have posted their secret keys.

For this, I immediately thought of the following two locations: GitHub (as seen earlier) and StackOverflow (which generously allows you to download every single comment, post and edit ever made on the platform through [archive.org](https://archive.org/details/stackexchange)).

For GitHub, I simply made a throwaway script which tried going over as many commits and files with the search term `secret_key` as possible, and for StackOverflow I iterated over each possible piece of text and tried to match possible secret keys with the following regular expression:

![Regular expression used to capture the secret keys](https://blog.paradoxis.nl/assets/img/figure-05-67c23519-1844.webp "Regular expression used to capture the secret keys")

After a week of scraping GitHub posts on the background of my VPS, and combing through every StackOverflow post, comment and edit ever made I was left with a total of **37069 unique secret keys.**

### Cracking the signature

By combining a wordlist, the cookie we just obtained and parts of Flask’s session management code; we’re able to validate each secret key against the cookie to see if the signature is valid. If no error is raised, we’ll know we have a valid signature, which means we’ll have figured out the server’s secret key!

![A sample session-cracking application](https://blog.paradoxis.nl/assets/img/figure-06-8f186930-1932.webp "A sample session-cracking application")

### Crafting a session cookie

If our script was successful, we should now have found the server’s secret key! By now taking the same code, but instead of ‘load’ the session we ‘dump’ the session, we can create a cookie with any data we like.

![Crafting a new session](https://blog.paradoxis.nl/assets/img/figure-07-a9725f94-1634.webp "Crafting a new session")

If we were now to make a request to the same server, it should accept our crafted cookie, as it matches the expected secret key, which should trick it into believing we’re logged in.

![Using our crafted cookie to bypass the server’s authentication](https://blog.paradoxis.nl/assets/img/figure-08-793d1f45-1932.webp "Using our crafted cookie to bypass the server’s authentication")

> I would like to point out that just because you can modify a session, doesn’t mean you’ll instantly be able to bypass an authentication mechanism. Not all systems are built the same and you’ll probably have to do some research to figure what and if it is possible to use this attack to your advantage.

## Flask Unsign

Due to the fact that I had to do quite some work to figure out *exactly how* Flask handled their sessions, I decided to put the code into an easy-to-use command line tool which lets you scan your own server’s for this issue.

To install the tool, simply head over to your terminal and install it using [pip](https://en.wikipedia.org/wiki/Pip_%28package_manager%29):

```bash
$ pip install flask-unsign[wordlist]
```

If you don’t want the fairly bulky wordlist file included and only want to use the code itself, you can simply omit the `[wordlist]` from the command.

```bash
$ pip install flask-unsign
```

If you wish to see the source code, you can find this over on my [GitHub](https://github.com/Paradoxis/Flask-Unsign).

### Usage

Flask-Unsign has three main use-cases: it lets you: Decode, Sign and Unsign (crack) a cookie, with built-in HTTP support, which prevents you from having to open up your browser.

![Cracking and crafting a session using Flask-Unsign](https://blog.paradoxis.nl/assets/img/figure-09-f78bdd88-2288.webp "Cracking and crafting a session using Flask-Unsign")

## The Numbers

By now you’re probably asking yourself: **What is the chance that this actually works in the real world?** To test this, Rik van Duijn (cool guy, go check out [his Twitter](https://twitter.com/rikvduijn?lang=en)) and I did a few queries on [Shodan](https://en.wikipedia.org/wiki/Shodan_%28website%29) to see how many public-facing devices were broadcasting that they might be running Flask.

![This resulted in around 88,000 servers](https://blog.paradoxis.nl/assets/img/figure-10-c37673d4-1880.webp "This resulted in around 88,000 servers")

We then narrowed it down to those who immediately set a session cookie *(not only due to the fact that Shodan can get pricey, but crawling each host until we find a session cookie might be very intrusive, would most likely melt my router, and probably result in a phone call from my ISP asking what the hell I’m up to).*

![This resulted in around 1,500 servers](https://blog.paradoxis.nl/assets/img/figure-11-131d3e03-1880.webp "This resulted in around 1,500 servers")

After downloading the results, I went to work to see exactly how many sessions I could successfully crack with a wordlist I created from a few days of scraping various data sources on the internet where people might post their secret keys.

> Just because the servers are running Werkzeug, doesn’t mean they’re running Flask. Furthermore we’re not taking applications whose information is stripped by another web server like Nginx, those who don’t instantly set a cookie or which are running behind a firewall into account. So take the following data with a grain of salt.

![Successfully cracking a little more than 28% of all valid sessions](https://blog.paradoxis.nl/assets/img/figure-12-e5b2c08b-2118.webp "Successfully cracking a little more than 28% of all valid sessions")

> While I initially started by brute-forcing the sessions on my MacBook, I quickly realised that MacBooks aren’t exactly cut out to run 32 CPU-slurping Python processes at once, so I switched to my (ever so slightly) more powerful gaming PC which obediently completed the task in under 20 minutes.

After weeding out the non-signed cookies (generally server-side cookies, or other frameworks which might use the same naming convention and base code as Flask), I was left with **1242** **valid** sessions. Passing each of these to [Flask-Unsign](https://github.com/Paradoxis/Flask-Unsign), resulted in **352 cracked** sessions which is a little over **28%**. Of these 352 sessions, only **78** **unique** secret keys were used.

![Top 10 most commonly used secret keys](https://blog.paradoxis.nl/assets/img/figure-13-5e0bf281-2059.webp "Top 10 most commonly used secret keys")

## Mitigation

There are multiple ways you could avoid this issue. The first and most obvious way of doing so is to simply **KEEP YOUR SECRET KEYS SECRET!** Apart from the obvious ones, the following tips should help you keep your server more secure.

### Making your secret key random

Stop using easy-to-guess secret keys; aim for something totally random instead. Ideally, you’d want to set your secret key to a random sequence of bytes each time you start your application, but this might be user-unfriendly as their session would expire each time your server is restarted.

The most practical solution is to simply generate a UUID. This can be done on most Unix-like systems by using the `uuid` or `uuidgen` command, or by running the following on a machine with Python:

```bash
$ python -c 'import uuid; print(uuid.uuid4());'
```

### Use server-side sessions

Not only do you prevent attackers from figuring out your secret key, using server-side sessions also makes it impossible for an attacker to look at the contents of your session, as the only thing they’ll get is a unique token.

One way of doing this in Flask is by installing the [Flask-Session](https://pythonhosted.org/Flask-Session/), package and initializing it when your application is being built.

### Sample Application

The following is a sample Flask application, which uses the previously-mentioned techniques to make sessions more robust.

```python title="Secured Flask Application"
# Requirements: Flask, Flask-Session
import os
from flask import Flask, session
from flask_session import Session


app = Flask(__name__)

app.config['SECRET_KEY'] = os.urandom(64)
app.config['SESSION_TYPE'] = 'filesystem'

Session(app)


@app.route('/')
def index():
    if 'logged_in' not in session:
        session['logged_in'] = False

    if session['logged_in']:
        return '<h1>You are logged in!</h1>'
    else:
        return '<h1>Access Denied</h1>', 403


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

## Special Thanks

Special thanks to [Rik van Duijn](https://twitter.com/rikvduijn?lang=en) for helping me out with general advice, and helping to generate the statistics showed earlier in this post.
