CVE-2025-68158

Authlib's Amnesia: The 1-Click Hijack via Global Cache

Alon Barad
Alon Barad
Software Engineer

Jan 9, 2026·6 min read

Executive Summary (TL;DR)

If you use Authlib with a Redis or Memcached backend for storing OAuth state, your application is vulnerable to a 1-click account takeover. The library failed to verify that the user finishing the OAuth flow is the same one who started it. Attackers can generate a valid OAuth token, send the link to a victim, and link the attacker's social account to the victim's profile.

A critical logic flaw in Authlib's OAuth implementation allows for Login CSRF and Account Takeover when a cache backend is used. By decoupling state verification from the user session, attackers can trick the application into accepting malicious OAuth callbacks.

The Hook: State of Confusion

OAuth is a dance. A complicated, multi-step tango between the User, the Client (your app), and the Provider (Google, GitHub, etc.). To keep everyone from stepping on each other's toes, we rely on the state parameter. It's effectively a CSRF token that says, 'Yes, I, the user currently on this site, actually requested this login attempt.'

Usually, this state is stored in a cryptographically signed session cookie. It's safe, it's boring, and it works. But developers love performance. They love scaling. So, they look at those session cookies and think, 'Why not offload this to Redis? It's faster!'

Enter Authlib. It supports offloading OAuth state to a cache backend like Redis or Memcached. Ideally, this changes where the data is stored, not who owns it. But in CVE-2025-68158, Authlib made a classic architectural blunder: it confused storage with identity.

By moving the state to a global cache, Authlib inadvertently created a 'global state bucket.' If you have the key (the state string), you can retrieve the data. It didn't matter if you were the user who put it there or a malicious actor who just happened to generate it. This is akin to a coat check where the attendant gives you your coat just because you shouted a random number, without asking to see your ticket.

The Flaw: Decoupling Session from State

The vulnerability lies in how FrameworkIntegration handles state retrieval when a cache object is present. In a secure implementation, the server must answer two questions during the OAuth callback:

  1. Is this a valid state that we generated?
  2. Does this state belong to the user currently sending the request?

Authlib <= 1.6.5 correctly answered question #1. It failed miserably at question #2. When a developer configured a cache, the library generated a cache key based solely on the application name and the state string (e.g., _state_myapp_randomstring123).

[!WARNING] The Logic Gap: The retrieval method get_state_data simply looked up this key in the global cache. If the key existed, it returned the data. It completely ignored the user's HTTP session cookie.

This means the state parameter ceased to function as a CSRF token. It became a 'bearer token' of sorts. Anyone who possessed a valid state string—which the attacker obviously has, because they generated it themselves—could force any user to consume that state. The application would see the valid state in Redis, assume everything was fine, and proceed to log the user in or link the account.

The Smoking Gun: Code Analysis

Let's look at the Python code that caused this mess. The vulnerability exists in authlib/integrations/base_client/framework_integration.py. Specifically, look at how get_state_data blindly trusts the cache.

The Vulnerable Code (<= 1.6.5):

def get_state_data(self, session, state):
    # The key is derived ONLY from the state string
    key = f'_state_{self.name}_{state}'
    if self.cache:
        # Fetches from global cache. No session validation!
        value = self.cache.get(key)
        if value:
            return json.loads(value)['data']
    return session.get(key)

Do you see the issue? The session argument is passed to the function but ignored if self.cache is set. The code assumes that if the data exists in Redis, it's valid for the current request.

The Fix (Commit 2808378):

The patch re-introduces session binding. Even if the data payload is in Redis, the reference to that payload must exist in the user's session cookie.

def get_state_data(self, session, state):
    key = f'_state_{self.name}_{state}'
    # NEW: Check the session first!
    session_data = session.get(key)
    if not session_data:
        return None
 
    if self.cache:
        # Only fetch from cache if session confirms ownership
        return self._get_cache_data(key)
    return session_data

Now, the application checks the user's pocket (session) for the ticket before going to the back room (Redis) to get the coat.

The Exploit: 1-Click to Pwnage

How do we weaponize this? We use a classic Login CSRF attack, but because of the state decoupling, it's highly reliable. The goal is to force the victim to log in using the attacker's identity, or link the attacker's OAuth provider to the victim's account.

The Attack Chain:

  1. Preparation: The attacker visits the target site and clicks "Login with Google." They proceed through the Google prompts.
  2. Interception: The attacker intercepts the final redirect from Google back to the application. They capture the URL containing ?code=EVIL_CODE&state=EVIL_STATE. They drop this request, so the code is not consumed yet.
  3. State Injection: At this exact moment, the vulnerable application has already written EVIL_STATE into its global Redis instance.
  4. The Trap: The attacker crafts a link: https://vulnerable-app.com/authorize/callback?code=EVIL_CODE&state=EVIL_STATE. They send this to the victim via email or a hidden iframe.
  5. Execution: The victim, already logged into the app, clicks the link. The app sees the state parameter.

The Result: The app asks Redis: "Do I know EVIL_STATE?" Redis says: "Yes." The app ignores that the victim's session never requested this. The app processes the callback, linking the Attacker's Google account to the Victim's user profile.

Now, the attacker simply goes to the site, clicks "Login with Google," and is logged straight into the victim's account.

The Impact: Why You Should Care

This isn't just about logging someone into the wrong account. That sounds annoying but harmless, right? Wrong. In modern applications, OAuth is often used for account linking.

If an application allows users to attach a GitHub or Google account to their existing profile (e.g., "Connect your GitHub to enable CI/CD"), this vulnerability is critical. An attacker can attach their GitHub account to the victim's corporate profile silently.

Once linked, the attacker has persistence. They can log in as the victim at any time, bypassing passwords and potentially MFA (since OAuth is often treated as a trusted factor). This leads to full account takeover, data exfiltration, and unauthorized actions performed under the guise of a legitimate user.

[!NOTE] Scope: This affects any Authlib implementation using cache. If you are using the default cookie-based storage, you are safe. But if you optimized for performance, you traded away your security.

Mitigation: Patching the Hole

The remediation is straightforward, but it requires immediate action if you are using cache backends.

Primary Fix: Update Authlib to version 1.6.6 or higher immediately. The patch ensures that a session key is required to unlock the cached state data.

Temporary Workaround: If you cannot upgrade, disable the cache configuration in your OAuth client. Revert to the default behavior where the entire state is stored in the session cookie. This might increase your header size, but it stops the account takeover.

Developer Lesson: Never treat a global cache as a trusted store for user-specific context without a secondary verification layer. Context is king. If data doesn't have a user ID or session ID attached to it, it's public property.

Fix Analysis (1)

Technical Appendix

CVSS Score
5.7/ 10
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N

Affected Systems

Python applications using AuthlibFlask/Django apps with Authlib OAuth clientsSystems using Redis/Memcached for OAuth state storage

Affected Versions Detail

Product
Affected Versions
Fixed Version
Authlib
Authlib
<= 1.6.51.6.6
AttributeDetail
CWE IDCWE-352 (CSRF)
Attack VectorNetwork
CVSS Score5.7 (Medium)
Privileges RequiredLow (User)
User InteractionRequired (1-click)
Exploit StatusPoC Available
CWE-352
Cross-Site Request Forgery (CSRF)

The application does not verify that the entity performing an action is the same entity that initiated the action, allowing for Cross-Site Request Forgery.

Vulnerability Timeline

CVE Published
2025-01-08
Patch Released (v1.6.6)
2025-01-08

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.