If you write Python and need to work with security, odds are you’ve crossed paths with cryptography. It’s a popular library designed to provide strong cryptographic primitives, making things like encryption, decryption, signatures, and more easy to use and secure—at least, that’s the goal.
But in 2017, cryptography version 1.8 introduced something called Cipher.update_into. This handy method promised to decrypt data straight into a provided buffer, saving memory allocations. Unfortunately, it came with a dangerous bug: one that survived from day one until February 2023 and could let you *mutate immutable objects* like Python’s own bytes.
That’s right: What shouldn’t ever change in Python *could* be mutated, leading to chaos like corrupted data, security leaks, and unpredictable crashes. This problem popped up in every cryptography version since 1.8.
Let’s break it down, see some code, look at the actual impact, and learn how to stay safe.
References
- GitHub Security Advisory GHSA-g452-c42w-7g2x
- NVD Entry CVE-2023-23931
- Original Fix PR on GitHub
- Changelog entry
The Bug: Mutating “Immutable” Bytes
Python’s bytes and other immutable types are meant to never change after creation. If something in Python changes a bytes object’s contents, the world turns upside down. That’s exactly what happened when users fed a bytes object into Cipher.update_into as the output buffer. It would accept it, write (low-level C code) directly into it, and... mutate it.
Unexpected bugs and hard-to-trace crashes.
- Possible security implications if you rely on Python’s immutability—maybe even leaking or tampering with secrets.
A Simple Example: How the Bug Happened
Suppose you’re encrypting data and want to be clever about memory usage. You use update_into with a buffer. But—oops!—you use a bytes object by mistake.
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
key = b'\x00' * 32
iv = b'\x00' * 16
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
data = b"this is some data123" # Must be a multiple of block size
# THIS IS BAD! Immutable bytes as output buffer
out_buf = b"\x00" * len(data) # Should use a bytearray instead
encryptor.update_into(data, out_buf) # This line would corrupt out_buf in vulnerable versions!
print(out_buf) # Mutated bytes object!
In normal Python, if you try to write into a bytes object, you get an error. But here, the C code in the cryptography library didn’t check, so your “immutable” buffer got changed.
The Exploit: Demonstrating the Mutation
Let’s see a more practical proof of the problem.
Code Snippet
plain = b'A' * 16 # AES block
out = b'Z' * 16 # This is immutable bytes
print(f"Before: {out}")
encryptor.update_into(plain, out)
print(f"After: {out}") # Would print different bytes in affected cryptography!
Expected (fixed) behavior:
Raises TypeError or similar: “output must be a writable buffer”.
Vulnerable behavior:
How Was It Fixed?
Starting with cryptography 39.. (February 2023), the code checks if the output buffer is writable. If not, it raises an exception before doing anything.
Relevant PR:
cryptography#9079
Fixed code (simplified)
if not output_buffer.writable():
raise TypeError("output buffer must be writable")
Safe code
out_buf = bytearray(len(data))
encryptor.update_into(data, out_buf) # This is correct!
Conclusion
CVE-2023-23931 is a striking reminder: even in high-quality packages, low-level bugs can break core expectations like object immutability in Python. Always keep your cryptography up-to-date, and if you use buffer protocols, double-check that your buffers are mutable!
For full details, see:
https://github.com/pyca/cryptography/security/advisories/GHSA-g452-c42w-7g2x
*Do you have legacy servers using old cryptography? Time to upgrade!*
Timeline
Published on: 02/07/2023 21:15:00 UTC
Last modified on: 02/16/2023 16:57:00 UTC