flawopen.com/Teardowns/cve-2026-104873-langgraph-sdk-auth-action-bypass
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]
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.threadsthat 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
- CWE reference for the weakness described in this guide: CWE-863
- OWASP API Security Top 10 — API5:2023 Broken Function Level Authorization
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
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]
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]
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
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
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
- ✓Inventory Python services using
langgraph-sdk; upgrade versions 0.1.45 through 0.4.3 to 0.4.4 or later.[1], [3] - ✓Search for
@auth.on.threads,@auth.on.assistants, and@auth.on.cronsdecorators that passactions=; review each selected handler and its fallback policy.[1], [2] - ✓Require every handler to authorize the current action and verify that the authenticated principal owns or may access the exact resource identifier in the request.[1]
- ✓Add regression tests for every declared action and for undeclared actions; assert that denied operations do not fall through to a permissive handler.[2]
- ✓Add cross-user tests for thread, assistant, and cron resources, including read, update, and delete operations wherever those actions are exposed.[1]
- ✓Review custom authorization registrations at startup and fail deployment on duplicate, empty, or unknown action lists rather than silently broadening handler scope.[2]
- ✓Do not infer safety from login alone: authentication identifies the caller, while each action still requires an explicit permission and ownership decision.
- ✓Configure a global default-deny handler for every custom-auth path not covered by action-scoped handlers. langgraph-api 0.10+ warns about uncovered paths, but a startup warning does not deny requests.[5], [1]