Anatomy of a Crypto Vulnerability

Before I describe the vulnerability, I want to give huge thanks to Ben Bangert and Alessandro Molina for quickly responding to my report, and to Paul Kehrer for reviewing and confirming my findings.

Sessions are a core part of many web applications. Put an opaque identifier (e.g. a UUID) in a cookie, then in your web app find the session in a database of some sort. The session might contain data like the currently logged in user, whatever.

For a variety of reasons, sometimes you don’t want to have the database on your backend. Instead you want to put the session data directly into a cookie. In those cases, generally you want to ensure your cookie has two properties: integrity and confidentiality. Integrity means a user should not be able to change the contents of their session (e.g. to change what user_id they’re logged in as), and confidentiality means they should not be able to see the contents of their session 1.

Beaker is a popular Python library for managing sessions. I was looking at it for reasons that I’ve completely forgotten at this point. Specifically, I was looking at its crypto. Beaker offers encrypted sessions, and I wanted to see how they were implemented.

Digging around a bit, I found an aesEncrypt function 2:

def aesEncrypt(data, key):
    cipher = AES.new(key, AES.MODE_CTR,
                     counter=Counter.new(128, initial_value=0))

    return cipher.encrypt(data)

Ok, AES in CTR mode. The first thing that jumped out to me is that the nonce (or counter here) is always b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'. CTR mode for block ciphers has one requirement: you must never reuse a (key, nonce) pair. If you reuse a pair, then any data encrypted under that pair becomes decryptable by the attacker. Since our nonce is constant, that means we must have a unique key for every encryption, or confidentiality is totally broken.

Let’s look to see where our key comes from.

A quick grep for aesEncrypt( 3 turns up:

nonce = b64encode(os.urandom(6))[:8]
encrypt_key = crypto.generateCryptoKeys(self.encrypt_key,
                                        self.validate_key + nonce, 1)
data = util.serialize(session_data, self.data_serializer)
return nonce + b64encode(crypto.aesEncrypt(data, encrypt_key))

What’s happening here? First we generate a 6-byte nonce from /dev/urandom and base64 encode it (note that this is different from the nonce in aesEncrypt).

Then we call generateCryptoKeys 4. What does it do?

def generateCryptoKeys(master_key, salt, iterations):
    return pbkdf2(master_key, salt, iterations=iterations, dklen=keyLength)

Doing a little substitution, this means that encrypt_key is derived by:

pbkdf2(self.encrypt_key, self.validate_key + nonce, iterations=1, dklen=32)

self.encrypt_key and self.validate_key are constant in this context. This means that if our nonce repeats, then the result of generateCryptoKeys repeats, which is passed to aesEncrypt as key.

If nonce here is 6 bytes, which are base64 encoded, how often does it repeat? We can ignore the base64 encoding since it doesn’t affect the entropy. 6 bytes of random data means 48 bits. So 2 ** 48 possible values. The birthday paradox means that we can, with 50% probability, expect a repeat after sqrt(2 ** 48) (or 2 ** 24) values. 2 ** 24 is a little under 17 million.

In short, if we observe 17 million cookies, there’s a 50% chance we’ll be able to decrypt a pair of them. That may not sound like much, but it’s completely unnecessary: we know how to build more secure cryptographic systems. It’s particularly bad for a general purpose library, where we have no idea how important the data being protected is.

The immediate-term solution is for Beaker to use a larger nonce size, which decreases the frequency of repeat values. A better long-term solution is to move away from custom cryptographic code and use safer, preprepared recipes, such as NaCL’s secret_box or Fernet (available for Python in PyNACL and pyca/cryptography), which have received extensive review from cryptographers and offer safer APIs.


  1. For some applications confidentiality isn’t actually important. But if you’re going to offer it, you should do it correctly. ↩︎

  2. https://github.com/bbangert/beaker/blob/8cc7e316df2ac90ea3f75db4052212c192376dec/beaker%2Fcrypto%2Fpycrypto.py#L20-L24 ↩︎

  3. https://github.com/bbangert/beaker/blob/8cc7e316df2ac90ea3f75db4052212c192376dec/beaker/session.py#L259-L270 ↩︎

  4. https://github.com/bbangert/beaker/blob/8cc7e316df2ac90ea3f75db4052212c192376dec/beaker%2Fcrypto%2F__init__.py#L39-L44 ↩︎