Sep 21, 2026·10 min read·5 visits
A session validation flaw in Hatchet allows attackers to bypass OAuth state verification using empty parameters, enabling Login CSRF and unauthorized account association.
A logic error in Hatchet's OAuth state validation mechanism allows unauthenticated remote attackers to bypass state parameter verification. By submitting an empty state parameter, attackers can exploit an equality collision with cleared session keys, facilitating Login Cross-Site Request Forgery (Login CSRF) or Session Fixation.
Hatchet is a distributed background task orchestration and work-dispatch platform designed for running durable workflows and managing AI agents at scale. Because it coordinates execution flows across multiple environments, secure identity management is a core structural requirement. The system integrates standard OAuth 2.0 authentication flows with popular third-party identity providers, including Google, GitHub, and Slack. These identity providers establish user authority, allowing workers and administrators to interact securely with the system management console.
The vulnerability, cataloged as CVE-2026-61687, manifests within the application's OAuth callback handler, specifically in the ValidateOAuthState helper function. The role of this function is to protect the user's login sequence against Cross-Site Request Forgery (CSRF). It does this by evaluating a cryptographically random, single-use state token that must match across the initiation and callback phases of the authentication flow. Due to an operational logic error, however, the token-cleaning phase fails to securely drop the validated state token from active memory, leaving a vacant key structure that can be targeted.
This architectural defect falls under the classification of Improper Authentication (CWE-287), Session Fixation (CWE-384), and Cross-Site Request Forgery (CWE-352). An unauthenticated network adversary can exploit this weakness by orchestrating a situation where a victim's active session is forcefully bound to the attacker's own external identity. The subsequent sections outline how this logic collision occurs, detail the vulnerable Go code patterns, and define the necessary actions required to secure affected systems.
The root cause of CVE-2026-61687 lies in the lifecycle management of the OAuth state validation token stored in the user's cookie-backed session. In a standard, securely implemented OAuth 2.0 flow, the application backend generates a unique random string and places it inside the user's session variables under a specific provider key (e.g., oauth_state_github). The same string is transmitted to the third-party Identity Provider (IdP) via the state query parameter during redirection. Upon successful user authentication at the IdP, the browser is redirected back to the Hatchet server callback endpoint, which must verify that the state parameter returned by the IdP matches the state parameter stored in the user's browser session.
To prevent authorization-binding replay attacks, once a state token is validated, it must immediately be destroyed. The developer intended to fulfill this requirement by modifying the session object during the final step of the validation helper. However, rather than calling a delete method to remove the state key from the session dictionary entirely, the application set the key's value to an empty string (""). This approach left the key present in the session map, associating the active key oauth_state_github with a value of "" for the remainder of the session's validity period.
This implementation creates a critical logic collision during subsequent validation checks. Go's map structure still reports that the key exists, meaning any query targeting that key will succeed. When an attacker redirects a victim to the OAuth callback URL with an empty state parameter (?state=), the callback handler fetches the parameter and queries the session store for the stored state. The application retrieves the empty string "" from the session, compares it against the empty string "" from the URL, and incorrectly concludes that the request is valid. This allows an unauthenticated attacker to inject their own authorization code into a victim's session, resulting in a successful session-binding sequence.
An analysis of the vulnerable source code in api/v1/server/authn/session_helpers.go illustrates the execution path leading to the state-bypass condition. The original code checks for the existence of the state key inside the session dictionary using a comma-ok idiom. However, because the key was previously cleared by setting its value to an empty string ("") instead of deleting it, the boolean existence check ok always evaluates to true. Below is the side-by-side comparison of the vulnerable logic and the corrected logic implemented in Hatchet version 0.91.1.
// VULNERABLE CODE PATH (Pre-v0.91.1)
func (s *SessionHelpers) ValidateOAuthState(
c echo.Context,
integration string,
) (isValidated bool, isOAuthTriggered bool, err error) {
stateKey := fmt.Sprintf("oauth_state_%s", integration)
session, err := s.ss.Get(c.Request(), s.ss.GetName())
if err != nil {
return false, false, err
}
// The key is present, even if its value is "", so ok is true
if _, ok := session.Values[stateKey]; !ok {
return false, false, fmt.Errorf("state parameter not found in session")
}
// If c.Request().URL.Query().Get("state") is "", this evaluates "" != ""
if c.Request().URL.Query().Get("state") != session.Values[stateKey] {
return false, false, fmt.Errorf("state parameters do not match")
}
// ... verification continues ...
// VULNERABILITY: Setting to empty string instead of deleting the key
session.Values[stateKey] = ""
session.Values["oauth_triggered"] = false
if err := session.Save(c.Request(), c.Response()); err != nil {
return false, false, fmt.Errorf("could not clear session")
}
return true, isOAuthTriggered, nil
}To correct this vulnerability, several structural adjustments were introduced. First, the application now explicitly rejects empty state parameters incoming from the HTTP query vector. Second, the session retrieval mechanism verifies that the stored state parameter is a non-empty string. Third, the comparison logic utilizes the constant-time comparison library to mitigate timing side-channel attacks. Finally, the application calls the native delete() function on the session values map to completely remove the state tracking key from memory, preventing any subsequent validation attempts against dead state structures.
// PATCHED CODE PATH (v0.91.1)
func (s *SessionHelpers) ValidateOAuthState(
c echo.Context,
integration string,
) (isValidated bool, isOAuthTriggered bool, err error) {
stateKey := fmt.Sprintf("oauth_state_%s", integration)
provided := c.Request().URL.Query().Get("state")
// Fix 1: Explicitly reject empty incoming state parameters
if provided == "" {
return false, false, fmt.Errorf("missing state parameter")
}
session, err := s.ss.Get(c.Request(), s.ss.GetName())
if err != nil {
return false, false, err
}
stored, ok := session.Values[stateKey].(string)
// Fix 2: Assert key presence AND verify it is not empty
if !ok || stored == "" {
return false, false, fmt.Errorf("state parameter not found in session")
}
// Fix 3: Use constant-time comparison to prevent timing leaks
if subtle.ConstantTimeCompare([]byte(provided), []byte(stored)) != 1 {
return false, false, fmt.Errorf("state parameters do not match")
}
// ... verification continues ...
// Fix 4: Physically remove keys from the map
delete(session.Values, stateKey)
delete(session.Values, "oauth_triggered")
if err := session.Save(c.Request(), c.Response()); err != nil {
return false, false, fmt.Errorf("could not clear session")
}
return true, isOAuthTriggered, nil
}Exploiting CVE-2026-61687 requires specific prerequisites but low overall execution complexity. First, the target Hatchet instance must have an active OAuth login integration enabled, such as Google or GitHub. Second, the victim must have successfully initiated and completed at least one OAuth login during their current browser session. This initial successful authentication leaves the oauth_state_<integration> session variable modified and resting at the default value of "". At this stage, the victim's session is in a vulnerable, primed state.
The attacker initiates the exploit by authenticating to the same OAuth provider (e.g., GitHub) using their own attacker-controlled account. Instead of completing the redirect back to Hatchet normally, the attacker intercepts the OAuth callback request to obtain their own unique authorization code (code parameter). The attacker then constructs a malicious callback link that references the Hatchet callback URL, incorporating their own authentication code but replacing the state parameter with an empty string: /api/v1/users/github/callback?code=ATTACKER_CODE&state=.
When the victim visits the malicious link or is forced to load it via an embedded resource, their browser transmits the request along with their active Hatchet session cookie. The Hatchet server processes the callback, extracts the empty state string, and compares it against the empty string stored in the victim's session cookie. Because they match, validation succeeds. The server processes the attacker's authorization code, retrieves the attacker's identity from the IdP, and binds the attacker's identity to the victim's session context, achieving a Login CSRF compromise.
The impact of CVE-2026-61687 is high, as it allows for severe session manipulation and unauthorized account access. Unlike traditional CSRF attacks that trigger unauthorized state modifications inside a victim's account, Login CSRF binds the attacker's external identity to the victim's active application context. If the victim continues using the system while bound to the attacker's identity, any workflows created, API tokens generated, or sensitive task data processed will be written to the attacker's workspace. The attacker can then log into their own account and inspect or control these assets.
Furthermore, because Hatchet orchestrates durable background tasks and schedules workflows, this vulnerability provides an indirect path to remote code execution (RCE) or sensitive data extraction. If an attacker can successfully bind their identity to a victim's active session, they can manipulate task parameters, view internal infrastructure tokens, or configure malicious step functions that execute on the victim's self-hosted runner infrastructure. The integrity of the automated orchestrations running within the organization is completely compromised.
The CVSS v3.1 score for this vulnerability is evaluated at 7.1 (High) with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:N. While the attack vector is network-based and authentication is not required, user interaction is necessary to lure the victim to the malicious redirection trigger. The compromise of integrity is ranked high because the trust boundaries within the user sessions are completely broken, allowing attackers to manipulate session context mapping.
The primary and recommended resolution for CVE-2026-61687 is to upgrade the Hatchet deployment to version 0.91.1 or higher. The patches introduced in this release resolve the logic error by checking for empty state parameters, comparing state variables using constant-time evaluation, and physically deleting the keys from the session map. After performing the upgrade, administrators should ideally invalidate all existing sessions, forcing users to re-authenticate and purging any legacy session structures that might still carry empty string keys.
In scenarios where immediate upgrading is not possible, organizations should implement temporary virtual patching at the routing layer. Using a reverse proxy, Web Application Firewall (WAF), or load balancer, administrators can configure rules to inspect and block requests directed at /api/v1/users/*/callback that contain empty or missing state parameters. For example, a ModSecurity rule can be implemented to identify incoming callback GET requests where the query string contains state= followed directly by an ampersand or the end of the string, dropping these connections immediately.
# Example Nginx snippet to mitigate CVE-2026-61687
location ~* ^/api/v1/users/(github|google|slack)/callback$ {
if ($arg_state = "") {
return 400 "Invalid State Parameter";
}
proxy_pass http://hatchet_backend;
}Additionally, developers must ensure that security hygiene is maintained across all session manipulation procedures. When handling single-use parameters or state verification tokens, always prefer deleting the respective key entirely from the underlying map structures instead of assigning zero values or empty strings. Regular static analysis and implementation of integration tests, such as the one developed for Hatchet's patch, should be integrated into continuous integration pipelines to prevent regression of this vulnerability class.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Hatchet hatchet-dev | < 0.91.1 | 0.91.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-384, CWE-352, CWE-287 |
| Attack Vector | Network (AV:N) |
| CVSS v3.1 Score | 7.1 (High) |
| Exploit Status | poc |
| CISA KEV Status | Not Listed |
The software reuse-validates session tokens or fails to properly invalidate state variables after use, enabling attackers to hijack or pre-determine a session context.
A high-severity access control vulnerability in ToolHive CLI before v0.30.1 and ToolHive Studio before v0.38.0 allows local containerized MCP servers to bypass network isolation. This enables malicious workloads to establish TCP/IP connections to administrative and control plane endpoints exposed on the host loopback interface.
AnyCable is a real-time communication server. Prior to version 1.6.15, its Pusher-compatible REST API suffered from an authentication bypass vulnerability because it failed to verify that the request body matched the signature-validated body_md5 parameter. This allows attackers to perform replay attacks with modified body contents.
A denial-of-service vulnerability exists in AnyIO prior to version 4.14.2. Standard error streams of process-pool workers are connected to an operating system pipe that is never drained by the parent process. This allows a worker to fill the pipe buffer and deadlock indefinitely.
CVE-2026-63349 is a critical privilege-dropping bypass vulnerability in the AnyIO asynchronous framework (versions 4.14.0 and 4.14.1) on POSIX platforms. Due to a variable assignment typo, supplementary groups specified by the developer are not correctly propagated to the execution backend, resulting in subprocesses retaining the parent process's elevated supplementary group permissions.
CVE-2026-63406 is an information disclosure vulnerability in AnyCable-go prior to version 1.6.15. The built-in telemetry client is enabled by default with a hardcoded public authentication token ('secret'). This client digests highly sensitive configuration parameters and command-line arguments, including JWT secrets and RPC secrets, into a stable SHA-256 fingerprint. This fingerprint is sent over public networks, exposing those administrative secrets to offline dictionary and brute-force attacks if intercepted.
CVE-2026-84992 is a Cross-Site Scripting (XSS) vulnerability affecting md-editor-v3 before version 6.5.4. It occurs because the fenced-code block language parser directly interpolates unescaped language metadata into unquoted HTML attributes inside the custom rendering callback. This bypasses the built-in XSSPlugin which runs during the parsing phase, before rendering.