FlawOpen

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

● CVE-2026-104873 · CVSS v4 7.6[1] · 高危
安全研究 · FlawOpen

LangGraph CVE-2026-104873:授权绕过

LangGraph SDK 0.1.45–0.4.3 忽略三个 Python 装饰器的操作筛选。请升级到 0.4.4。 具体影响取决于处理器是否检查权限。[1]

CVSS 向量CVSS: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]
💡 通俗易懂的原理解析 (ELI5)

想象一栋楼的前台只负责收件,但路由系统把所有访客都送到前台。如果前台只确认访客进过大楼,他们就可能进入其他柜台本应锁住的房间。受影响 SDK 注册资源处理器时忽略 actions=,因此处理器也可能收到读取、更新或删除请求。只有处理器本身没有执行权限检查时才有风险。[1]

核心概念与专有名词

资源授权装饰器
为资源操作注册授权处理器的 Python 装饰器。[1], [2]
操作筛选
actions= 选择处理器的操作。[1], [2]
回退处理器
没有专用匹配时使用的通用处理器。[1], [2]
CWE-863
授权不正确:检查未落实预期权限。[1]

框架映射

根本原因剖析 (Root Cause)

0.1.45–0.4.3 接受 actions=,却将处理器注册到资源通配符 * 下。LangGraph 可能先选择它而不是通用回退处理器。如果函数只授权预期操作,其他操作就可能绕过回退检查。只有应用依赖被跳过的检查且处理器未自行验证操作和所有权时,已认证用户才可能访问他人资源。0.4.4 校验列表并逐个注册操作;未提供 actions= 时仍有意保留通配行为。公告列为 CWE-863、CVSS 4.0 7.6 High。这是依赖配置的授权缺陷,不是未认证绕过,也不影响所有 LangGraph 部署。 受影响的装饰器为:@auth.on.threads / @auth.on.assistants / @auth.on.crons. 修复的重要边界:SDK 改动会按声明的操作限定处理器注册;但如果应用未配置全局默认拒绝处理器,未匹配的自定义授权路径仍会被允许。0.4.4 更新日志说明 langgraph-api 0.10+ 会在启动时警告未覆盖路径;该警告用于发现问题,并不执行访问控制。[1], [2], [5]

攻击执行流程分解

步骤 1

配置

使用 SDK 0.1.45–0.4.3,并在受影响装饰器中指定 actions=。[1], [4]

步骤 2

注册

处理器原本只应处理某项操作。[1]

步骤 3

通配

SDK 将处理器关联到资源的所有操作。[1], [2]

步骤 4

检查

处理器可能未验证操作权限和所有权。[1]

步骤 5

修复

升级到 0.4.4,测试操作、回退逻辑和所有权。[1], [2], [3], [5]

源代码对比:漏洞与安全实现

易受影响片段展示旧行为:_ResourceOn.__call__ 无论 actions= 如何都使用 *。修复会校验列表并逐个注册操作。这是源码片段,不是完整模块替换。请升级,并要求处理器检查操作和资源所有权。 源代码位置: libs/sdk-py/langgraph_sdk/auth/__init__.py, method _ResourceOn.__call__.[1], [2]

源码摘录: 按操作注册处理器

存在漏洞的实现
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

工程与系统安全加固清单

参考来源