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 session1.
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 function2:
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 generateCryptoKeys4. 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.
|||For some applications confidentiality isn't actually important. But if you're going to offer it, you should do it correctly.|
Hi, I'm Alex. I'm currently at a startup called Alloy. Before that I was a engineer working on Firefox security and before that at the U.S. Digital Service. I'm an avid open source contributor and live in Washington, DC.