Jan 20, 2026·5 min read·57 visits
If you use Turbo Frames with cookie-based sessions (like default Rails), a slow network request from a frame can arrive *after* a user logs out. Because the browser processes the 'Set-Cookie' header from the delayed response immediately, the old session cookie is resurrected, and the user is re-authenticated without their knowledge.
A critical race condition in Hotwired Turbo allows delayed background requests to restore destroyed session cookies, effectively logging a user back in after they have signed out.
Hotwired Turbo (formerly Turbolinks) is the darling of the Ruby on Rails world. It promises the snappy feel of a Single Page Application (SPA) without the crushing complexity of React or Vue state management. It does this by intercepting links and form submissions, fetching HTML in the background, and swapping out the body <body> or specific <turbo-frame> elements.
But here's the thing about doing everything in the background: asynchronicity is hard. When you fire off a dozen requests to populate different frames on a dashboard, you lose the linear guarantee of a traditional page load. You are effectively throwing packets at the server and hoping they come back in an order that makes sense.
CVE-2025-66803 describes a scenario where that hope dies. It's a classic race condition, but it's not happening in your database or your threads—it's happening between your user's mouse click and the browser's network stack. It turns out, making things feel "fast" can sometimes make security remarkably loose.
To understand this vulnerability, you have to understand a fundamental behavior of web browsers that most developers ignore: The Network Stack and the DOM are decoupled.
When a JavaScript application (like Turbo) initiates a fetch() request, the browser handles the networking. If the server responds with HTTP headers, the browser processes them immediately. Crucially, if the response includes a Set-Cookie header, the browser updates its cookie jar before the JavaScript runtime even gets a chance to look at the response body or decide if it still cares about the data.
In the context of Turbo, imagine a <turbo-frame> requesting a slow widget. While that request is hanging in limbo, the user clicks "Logout". The application processes the logout, the server destroys the session, and the browser clears the cookie. Clean and secure, right?
Wrong. That slow widget request is still alive. A second later, it finally completes. The server, which processed that request before the session was destroyed (or concurrently), sends back a 200 OK with the Set-Cookie header containing the old, valid session ID. The browser, being a helpful idiot, sees the cookie and dutifully writes it back into the jar. The zombie session is born.
The fix for this is elegant but highlights exactly what was missing. The FrameController class in Turbo manages the lifecycle of frame elements. In versions prior to 8.0.21, when a frame was removed from the DOM (disconnected)—for example, when the page body was replaced during a logout redirect—the pending fetch request was simply orphaned.
The patch introduces the AbortController API to explicitly kill these requests. By calling .cancel(), Turbo signals the browser to sever the connection immediately, preventing the processing of late response headers.
Here is the critical diff in src/core/frames/frame_controller.js:
// BEFORE: The disconnect method didn't clean up network traffic
disconnect() {
// ... code removing event listeners ...
}
// AFTER: Explicitly cancelling the fetch request
disconnect() {
// ...
if (!this.element.hasAttribute("recurse")) {
this.#currentFetchRequest?.cancel() // <--- The Kill Switch
}
}
// Managing the loading state
sourceURLChanged() {
if (this.#isIgnoringChangesTo("src")) return
if (!this.sourceURL) {
this.#currentFetchRequest?.cancel() // <--- Stop fetching if src is cleared
}
// ...
}This .cancel() method bubbles down to trigger AbortController.abort(). This is the digital equivalent of hanging up the phone before the telemarketer (or in this case, the server) can finish their sentence.
Let's construct a realistic attack scenario. This works best against users on slow connections or applications with heavy server-side processing.
The Setup:
CookieStore (client-side session storage) and Turbo.<turbo-frame id="notifications" src="/notifications/recent"> that loads automatically.The Attack Chain:
notifications frame initiates a fetch request./notifications/recent endpoint takes 2 seconds to query the database./notifications/recent response arrives. It was generated while the user was still logged in, so it contains the Set-Cookie header with the valid encrypted session string.> [!WARNING] > This is particularly dangerous because from the user's perspective, the UI confirmed they were logged out. The betrayal happens entirely invisibly in the background.
If you are running @hotwired/turbo below version 8.0.21, you are vulnerable. The primary mitigation is obviously to upgrade.
However, this vulnerability also serves as a stark reminder of why Server-Side Sessions are superior to Client-Side Cookie Stores (like Rails' default).
If you use a server-side store (Redis, Memcached, Database):
If you stick with stateless cookie sessions (JWTs or encrypted cookies), you are trusting the client to hold the state. If the client (browser) gets confused, your security model collapses. Upgrade Turbo, but also consider architecting your auth system to not trust the browser quite so much.
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
@hotwired/turbo Hotwired | < 8.0.21 | 8.0.21 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-362 (Race Condition) |
| Attack Vector | Network / User Interaction |
| CVSS | High (Estimated) |
| Vulnerability Type | Session Restoration |
| Impact | Authentication Bypass |
| Patch Status | Released (v8.0.21) |
The software does not properly cancel asynchronous tasks when the state changes, leading to a race condition where a delayed response affects the application state after it has transitioned.
CVE-2026-77637 is a privilege scope bypass vulnerability in Cloudreve. It allows authenticated clients possessing read-only administrative credentials to access sensitive administrative tool endpoints that should require write-level permissions.
An incorrect authorization vulnerability (CWE-863) in Gardener's customverbauthorizer admission plugin allows project administrators lacking the manage-members permission to inject arbitrary Group or ServiceAccount subjects, granting unauthorized access to project resources.
Cloudreve versions prior to 4.18.0 contain a Server-Side Request Forgery (SSRF) vulnerability. The application's validation logic fails to canonicalize various IPv4-in-IPv6 transition formats, such as NAT64, 6to4, and Teredo addresses. Consequently, an authenticated user with remote-download permissions can issue requests that bypass SSRF network boundaries, enabling connection routing to loopback, private, or cloud metadata endpoints.
Hatchet V1 Dispatcher before version 0.95.3 fails to enforce proper tenant boundaries when managing active stream connections for durable task completions. Because the global lookup map is keyed solely by task external identifiers, authenticated attackers who obtain a victim's task UUID can register a stream subscription and receive task results belonging to another tenant.
CVE-2026-88978 is a critical cross-tenant data exposure vulnerability in Hatchet, a platform for orchestrating background tasks and durable workflows. The flaw exists in the durable-task event retrieval system where client-supplied task, node, and branch UUIDs are resolved via the ListSatisfiedEntries database query without verifying the tenant ownership of the requesting worker context.
An unauthenticated timing oracle vulnerability exists in Traefik's BasicAuth middleware from version 3.6.11 up to (but not including) 3.7.13. By utilizing a request coalescing mechanism (singleflight.Group) that relies on server-side stored secret hashes for key generation, the software introduces a timing discrepancy. Concurrent requests targeting non-existent usernames generate identical singleflight keys and coalesce, resulting in accelerated response times. Conversely, requests targeting valid usernames produce distinct keys and execute independently, allowing remote attackers to systematically enumerate valid usernames.