FlawOpen

flawopen.com/Teardowns/cve-2026-104873-langgraph-sdk-auth-action-bypass

● CVE-2026-104873 · CVSS v4 7.6[1] · High
FlawOpen Security Research

LangGraph CVE-2026-104873: Auth Bypass

LangGraph SDK 0.1.45–0.4.3 ignored actions= on three Python auth decorators, risking permission bypass. Upgrade to 0.4.4.[1]

CVSS vectorCVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N[1]
💡 Plain English Explainer (ELI5)

Imagine a building receptionist who is told to handle only deliveries, but the routing system sends every visitor to that desk. If the receptionist checks only whether someone entered the building, visitors may reach rooms that another desk would have kept locked. In affected LangGraph SDK versions, the actions filter for create-only handling was ignored when registering a resource handler, so it could receive reads, updates, or deletes too. The handler still had to make the relevant permission check for the exposure to matter.[1]

Core Concepts & Subsystem Terms

Resource-scoped authorization decorator
A Python decorator such as @auth.on.threads that registers an authorization handler for operations on a particular resource type.[1], [2]
Action filter
The actions= argument selects operations such as create, read, update, delete, or search that a handler is intended to process.[1], [2]
Fallback handler
A broader authorization handler used when no resource-specific handler matches. In the affected SDK, the wildcard resource handler could be selected first, so fallback checks did not run.[1], [2]
CWE-863
Incorrect Authorization: an access-control check is performed incorrectly or does not enforce the intended permission decision.[1]

Framework mappings

Root Cause Analysis

In langgraph-sdk 0.1.45 through 0.4.3, @auth.on.threads(actions=[...]), @auth.on.assistants(actions=[...]), and @auth.on.crons(actions=[...]) accepted an actions argument but registered the decorated handler under the resource wildcard *. LangGraph's handler selection could choose that resource-level handler before a broader fallback handler. If the decorated function only authorized the action it was written for, the same function could then receive other actions without the fallback's checks. An authenticated caller could access another user's resource only where the application depended on those skipped checks and the selected handler did not independently enforce the action and ownership rules. The upstream fix in 0.4.4 validates the action list and registers the handler separately for each selected action; omitting actions= intentionally retains wildcard behavior. The advisory assigns CWE-863 and CVSS 4.0 7.6 High. This is a Python SDK configuration-dependent authorization flaw, not an unauthenticated bypass and not a flaw in every LangGraph deployment. Important fix boundary: the SDK change scopes registrations for declared actions, but unmatched custom-auth paths still remain allowed unless the application configures a global default-deny handler. The 0.4.4 changelog says langgraph-api 0.10+ warns about uncovered paths at startup; treat that warning as detection, not enforcement.[1], [2], [5]

Step-by-Step Attack Flow

Step 1

Confirm the affected configuration

The deployment must use langgraph-sdk from 0.1.45 through 0.4.3 and one of the three affected resource decorators with actions=. The issue does not affect every SDK user or every custom authorization handler.[1], [4]

Step 2

The application registers an action-specific handler

An application may use a handler intended only for thread creation, while relying on another handler for reads, updates, or deletes.[1]

Step 3

The affected decorator registers a wildcard

The vulnerable SDK ignores the selected action list during registration and stores the handler for every action on that resource. The resource-specific match can take precedence over a broader fallback handler.[1], [2]

Step 4

An authenticated request reaches incomplete checks

A signed-in caller can reach an unintended operation if the wildcard-selected function lacks that operation’s authorization or ownership check. A handler that independently checks every action and resource owner remains protective.[1]

Step 5

Upgrade and test each action boundary

Upgrade to langgraph-sdk 0.4.4 or later. Verify that action-specific handlers are selected only for their declared actions, that other operations reach the intended policy, and that each request checks ownership of the target resource.[1], [2], [3], [5]

Source Code: Flaw vs. Secure Implementation

The vulnerable excerpt shows old _ResourceOn.__call__: both decorator forms registered the wildcard regardless of actions=. The fixed excerpt follows upstream resource/action validation, rejects empty, invalid, duplicate, and already-registered entries, then registers each selected action separately. Type annotations and unrelated return-type scaffolding are omitted; control flow and registration behavior match the patch. Upgrade the SDK, and keep action and resource-ownership checks in application handlers. Upstream source path: libs/sdk-py/langgraph_sdk/auth/__init__.py.[1], [2]

Source excerpt: libs/sdk-py/langgraph_sdk/auth/__init__.py — _ResourceOn.__call__ handler registration

UNPATCHED FLAW
def __call__(self, fn=None, *, resources=None, actions=None):
    if fn is not None:
        _validate_handler(fn)
        return typing.cast("_ActionHandler", _register_handler(
            self.auth, self.resource, "*", fn
        ))

    def decorator(handler):
        _validate_handler(handler)
        return typing.cast("_ActionHandler", _register_handler(
            self.auth, self.resource, "*", handler
        ))

    return decorator
HARDENED SECURE PATCH
def __call__(self, fn=None, *, resources=None, actions=None):
    def decorator(handler):
        _validate_handler(handler)
        if resources is None:
            resource_list = [self.resource]
        elif isinstance(resources, str):
            resource_list = [resources]
        elif isinstance(resources, Sequence):
            resource_list = list(resources)
        else:
            raise TypeError("resources must be a string or sequence of strings")
        if resource_list != [self.resource]:
            raise ValueError("use @auth.on(...) for other resources")

        if actions is None:
            action_list = ["*"]
        elif isinstance(actions, str):
            action_list = [actions]
        elif isinstance(actions, Sequence):
            action_list = list(actions)
        else:
            raise TypeError("actions must be a string or sequence of strings")
        if not action_list:
            raise ValueError("actions must not be empty")
        if not all(isinstance(action, str) for action in action_list):
            raise TypeError("actions must be strings")

        valid_actions = {
            value.action for value in vars(self).values()
            if isinstance(value, _ResourceActionOn)
        }
        invalid_actions = (
            sorted(set(action_list) - valid_actions) if actions is not None else []
        )
        if invalid_actions:
            raise ValueError("invalid action for this resource")
        if len(action_list) != len(set(action_list)):
            raise ValueError("actions must not contain duplicates")

        for action in action_list:
            if (self.resource, action) in self.auth._handlers:
                raise ValueError("handler already registered for action")
        for action in action_list:
            _register_handler(self.auth, self.resource, action, handler)
        return handler

    if fn is not None:
        return decorator(typing.cast("_ActionHandler", fn))
    return decorator

Engineering & System Hardening Checklist

Sources