Feb 23, 2026·6 min read·30 visits
Authlib < 1.6.6 failed to bind OAuth 'state' data to user sessions when using server-side caching (e.g., Redis). Attackers could generate a valid state, send the callback URL to a victim, and hijack the account via Login CSRF.
OAuth is a complex beast, and the `state` parameter is its primary defense against Cross-Site Request Forgery (CSRF). In Authlib versions prior to 1.6.6, a critical logic error occurred when developers opted for performance by storing OAuth states in a backend cache (like Redis or Memcached) instead of cookies. The library failed to cryptographically bind the cached state to the user's browser session. This decoupling allowed attackers to generate a valid OAuth flow, harvest the URL, and trick a victim into consuming it—effectively performing a Login CSRF or account linking attack with a single click.
If you build Python web applications, you probably use Authlib. It is the Swiss Army knife of OAuth and OpenID Connect, powering authentication for thousands of Flask, Django, and Starlette apps. It abstracts away the misery of the OAuth 2.0 handshake, handling the redirect dances and token exchanges so you don't have to.
The cornerstone of OAuth security is the state parameter. It is a random token generated by the client (your app) to prevent CSRF. It says, "I am the one who started this request, and I am the one finishing it." Without it, anyone could force your browser to log in to their account, or worse, link their rogue Google account to your administrative profile.
Here is the twist: Authlib handled this perfectly fine in its default mode (storing state in cookies). But developers love speed. We love scaling. So, we switch the storage backend to Redis or Memcached to keep our cookies small. In Authlib versions before 1.6.6, flipping that switch was like locking your front door but leaving the keys taped to the outside of the window. The library stopped checking who owned the state, and only checked if the state existed.
To understand this bug, imagine a valet parking service. Normally, you hand over your keys and get a ticket (the state). When you return, you show your ticket, and the valet checks two things: 1) Is this a valid ticket? and 2) Does the face matching this ticket look like the guy who dropped off the car?
Authlib's cache-backed implementation skipped step 2.
In framework_integration.py, the logic for retrieving state data looked something like this (simplified):
# The Vulnerable Logic
def get_state_data(self, session, state):
key = f"_state_{self.name}_{state}"
if self.cache:
# LOOKUP ONLY BY KEY
value = self.cache.get(key)
return valueDo you see the issue? The get_state_data function takes a session object, but it ignores it if a cache is configured. It constructs a lookup key based entirely on the state parameter found in the URL.
This means the state was no longer a secret bound to the user's browser session. It became a global, floating token. If I, the attacker, start a login flow, Authlib writes _state_google_XYZ123 to Redis. If I then send you a link containing state=XYZ123, Authlib looks up that key in Redis, finds it, and says, "Looks legit! Proceed."
The fix, introduced in commit 2808378611dd6fb2532b189a9087877d8f0c0489, reveals exactly how the maintainers patched the hole. They implemented a "double-binding" strategy. Even if the heavy data lives in Redis, a reference must exist in the user's session cookie.
Here is the critical diff:
def get_state_data(self, session, state):
key = f'_state_{self.name}_{state}'
+ # VERIFY THE SESSION HAS THIS KEY
+ if not session.get(key):
+ return None
+
if self.cache:
return self.cache.get(key)Before the patch, the code blindly trusted the cache. After the patch, the code first checks session.get(key). If the user's browser doesn't have the specific cookie indicating they started this flow, the server returns None, and the attack fails. It forces the state to exist in both the shared cache (Redis) and the private session (Cookie).
Let's weaponize this. We are going to perform a classic Login CSRF attack. The goal is to force the victim to log in using the attacker's credentials, or to link the attacker's social profile to the victim's existing account.
The Setup:
target-app.com/login/google.state=EVIL_STATE, stores it in Redis, and redirects the Attacker to Google.The Trap:
4. Attacker constructs the callback URL: https://target-app.com/authorize?code=ATTACKER_CODE&state=EVIL_STATE.
5. Attacker sends this link to the Victim via email/chat.
The Execution:
6. Victim clicks the link.
7. Target App receives the request. It extracts state=EVIL_STATE.
8. Vulnerable Logic checks Redis for _state_google_EVIL_STATE. It exists! (Because the attacker put it there).
9. Target App logs the user in.
The Payoff: If the application supports "Social Account Linking," and the victim was already logged in, the attacker's Google account is now linked to the victim's profile. The attacker can now log in as the victim at any time using their own Google credentials.
While NVD rates this as a Medium (5.7) because it requires user interaction, the practical impact is often Critical. Identity is the perimeter. If I can compromise your identity layer, I don't need an RCE.
In scenarios where this vulnerability is used for Account Linking, the impact is persistent Account Takeover (ATO). The attacker gains a permanent backdoor into the user's account without changing the password.
In Login CSRF scenarios (where the victim is logged in as the attacker), the impact is subtle but dangerous. The victim might upload sensitive documents or credit card details, thinking they are in their own account, effectively handing that data directly to the attacker. For SaaS platforms handling sensitive data, this is a privacy nightmare.
The remediation is straightforward: Update Authlib to version 1.6.6 or higher.
If you cannot update immediately, you must modify your FrameworkIntegration or custom OAuth client setup. You need to ensure that whatever storage backend you use, you are cross-referencing the state with the user's current session.
> [!NOTE]
> If you are using FileStorage or SQLAlchemy storage for states, check if your implementation relies on FrameworkIntegration. The vulnerability specifically affects the default logic where cache is passed to the registry.
After patching, ensure you rotate all active sessions. The patch changes how states are validated, so pending OAuth flows might fail immediately after deployment (a small price to pay for security).
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Authlib Authlib | < 1.6.6 | 1.6.6 |
| Attribute | Detail |
|---|---|
| Vulnerability Type | CWE-352: Cross-Site Request Forgery (CSRF) |
| Attack Vector | Network (Pre-generated State) |
| CVSS v3.1 | 5.7 (Medium) |
| Impact | Account Takeover / Account Linking |
| Fixed Version | 1.6.6 |
| Affected Component | authlib.integrations.base_client.framework_integration |
The application does not verify that the OAuth state parameter is bound to the user's current session, allowing CSRF attacks.
CVE-2026-49858 is a vulnerability in API Platform Core's JSON:API and HAL item normalizers where conditionally secured attributes are cached globally in memory. When deployed in long-running PHP execution environments such as FrankenPHP worker mode, Swoole, or RoadRunner, this persistent caching bypasses property-level security constraints, allowing unprivileged users to access sensitive, unauthorized fields cached during privileged requests.
CVE-2026-5078 is a log injection vulnerability in Morgan, the widely deployed Node.js HTTP request logging middleware. The vulnerability arises because the ':remote-user' logging token decodes and outputs basic authentication usernames containing control characters, such as Carriage Return (CR) and Line Feed (LF), without sanitization. An unauthenticated attacker can bypass native HTTP header parsers by Base64-encoding CRLF sequences in the Authorization header. When Morgan logs the request, these control characters force newlines in the log stream, enabling log forging, SIEM evasion, and system activity spoofing.
CVE-2026-48861 is a client-side HTTP request-line CRLF (Carriage Return Line Feed) injection vulnerability in the popular Elixir HTTP client library, Mint. The vulnerability permits HTTP Request Splitting and HTTP Request Smuggling when an application forwards untrusted, attacker-controlled inputs to Mint's HTTP client requests as either the HTTP request method or target. By embedding CRLF characters within these parameters, an attacker can terminate the request line prematurely, inject malicious headers, or pipeline entirely independent requests. These smuggled requests are then processed by upstream or downstream proxy servers as separate HTTP queries on the same TCP connection. While Mint version 1.7.0 introduced target validation to secure the request target, the HTTP request method parameter remained completely unvalidated. This flaw allows attackers to bypass routing filters, access restricted internal APIs, or poison HTTP caches under default configurations.
An Inconsistent Interpretation of HTTP Requests (HTTP Request/Response Smuggling) vulnerability in the Elixir Mint HTTP client allows attacker-controlled HTTP/1 servers to desynchronize response framing on shared connections due to over-lenient parsing of sign-prefixed Content-Length headers.
An allocation of resources without limits or throttling vulnerability in Elixir Mint allows an attacker-controlled HTTP/2 server to exhaust memory in a Mint client. The vulnerability is exploited by sending a HEADERS frame without the END_HEADERS flag followed by an infinite stream of CONTINUATION frames. Because the client lacks limits on the incoming header-block accumulator, the client continuously consumes memory until an out-of-memory crash occurs.
CVE-2026-48596 is an Improper Neutralization of CRLF Sequences in HTTP Headers (HTTP Request/Response Splitting, CWE-113) in the Elixir Tesla HTTP client. The flaw resides in how multipart content-type parameters are joined and serialized, enabling attackers to inject arbitrary headers or split HTTP requests when applications pass untrusted inputs to the parameters of multipart uploads.