Introduction

pickle is Python’s built-in serialization/deserialization library. It allows users to capture Python state in a format that is transferable between interpreters. It has long been a rich source of bugs for users who rely on it to deserialize untrusted content. What makes pickle more dangerous than other serialization/deserialization utilities, such as JSON, is an object’s ability to override a special method called __reduce__, which embeds a callable into the pickled object. Upon deserialization, the callable can execute arbitrary Python code.

oauth2client is a deprecated Google library that implements OAuth 2.0 and integrates with Flask, a Python web framework. Its pickle deserialization is reachable from attacker-controlled input. If an attacker can forge a session cookie, that cookie can contain a payload that executes arbitrary code on the host before authentication.

In this post, I explain the bug, demonstrate the exploit, and alert any downstream users I may have missed.

The Bug

There are two functions I will highlight to help explain the bug: the source of the legitimate pickled OAuth 2.0 flow and the vulnerable destination of the forged pickle payload.

  1. The _make_flow() function, which produces the serialized flow.

     flow = client.OAuth2WebServerFlow(
         client_id=self.client_id,
         client_secret=self.client_secret,
         scope=self.scopes,
         # ...
     )
     flow_key = _FLOW_KEY.format(csrf_token)
     session[flow_key] = pickle.dumps(flow) # ← pickled, stored in session
    
  2. The _get_flow_for_token() function, which deserializes the pickle.

     flow_pickle = session.pop(
         _FLOW_KEY.format(csrf_token), None)
     if flow_pickle is None:
         return None
     else:
         return pickle.loads(flow_pickle) # ← deserialized from session
    

The cookie signature is validated before _get_flow_for_token() is called.

According to the project’s recommended default setup, the Flask app is instantiated with the oauth2client library integration and initialized with a SECRET_KEY. The key suggested by Google’s Bookshelf example is 'secret'. The oauth2client docstring uses 'your-secret-key'. This secret key is used to sign a cookie using HMAC. The cookie contains a pickled OAuth flow object.

The attacker must know or correctly guess the signing key. A wrong guess fails Flask’s cookie signature check, so the forged session is not loaded. The sequence of operations that leads to exploitation is as follows:

sequenceDiagram
    participant Attacker
    participant App as Flask app

    Attacker->>Attacker: Create new pickle from payload defining __reduce__
    Attacker->>Attacker: Build session with new pickle and matching CSRF token
    Attacker->>Attacker: Sign forged session cookie with candidate key
    Attacker->>App: Send forged cookie with OAuth callback
    App->>App: Validate cookie signature
    App->>App: Validate callback state and matching CSRF token
    App->>App: _get_flow_for_token() calls pickle.loads()
    Note over App: Pickle payload executes during deserialization

To validate the vulnerability, I created a simple proof of concept (PoC) that writes a file to the target server’s filesystem. Here is an excerpt:

class RCEPayload:
    def __reduce__(self):
        return (eval, ("open('poc_proof.txt', 'w').write('RCE achieved')",))

When pickle.loads is called, eval is run. When I ran the PoC in my test environment with oauth2client 4.1.3, Flask 3.1.3, and Python 3.13.2, I saw poc_proof.txt written to disk with the marker text.

Disclosure

I disclosed this vulnerability and the exploit PoC to Google on February 22, 2026. The report was marked as WONTFIX due to the project’s archived state, even though PyPI Stats reported 24,545,418 downloads of oauth2client in the last month when checked on October 11, 2026. These counts measure downloads, not unique users or vulnerable deployments. I am sharing these details without a fix strictly for educational purposes and to help downstream users mitigate the issue.

If your app is using:

from oauth2client.contrib.flask_util import UserOAuth2
oauth2 = UserOAuth2(app)

and you are using a weak secret key, then you may be vulnerable.

Brief Aside: Cracking HMAC Signatures

Assuming a weak secret, the HMAC-signed cookie can be brute-forced offline without server interaction. You only need one cookie/signing key. Hashcat 7.1.2 gives 2.53 billion guesses per second for mode 29100 (Flask Session Cookie) on my GPU (NVIDIA GeForce RTX 5080).

Signing key choice Full search time Average time
Random 8 digits <1 second <1 second
Random 8 lowercase letters ~83 seconds ~41 seconds
Random 10 lowercase letters ~15.5 hours ~7.7 hours
Random 8 letters and digits ~24 hours ~12 hours
Random 10 letters and digits ~10.5 years ~5.3 years
Random 16 hexadecimal digits ~231 years ~115 years
Random 32 hexadecimal digits ~4 × 10²¹ years ~2 × 10²¹ years

Lessons Learned

  • Probably avoid pickle. This OAuth flow could have been JSON.
  • Use google-auth instead of the deprecated oauth2client.
  • Don’t use a hard-coded, weak, or shared example SECRET_KEY.

References

oauth2client 4.1.3 Flask integration: Source for _make_flow(), _get_flow_for_token(), and callback handling.

Google’s Bookshelf example: Example Flask application that sets SECRET_KEY to 'secret'.

Python 3.13 pickle documentation: Serialization behavior, __reduce__, and the warning against unpickling untrusted data.

Flask configuration documentation: The role of SECRET_KEY in signing session cookies and guidance on choosing a secure value.

HMAC overview: Background on keyed message authentication.

oauth2client download statistics: PyPI Stats reported 24,545,418 downloads in the last month when checked on October 11, 2026. This is a download count, not a count of vulnerable deployments.

Hashcat example hashes: Supported hash modes and example hashes. The GPU throughput reported above is from my own measurement.