FlawOpen

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

● CVE-2026-104873 · CVSS v4 7.6[1] · Alta
Investigación · FlawOpen

LangGraph CVE-2026-104873: bypass de autorización

LangGraph SDK 0.1.45–0.4.3 ignoraba filtros de acción en tres decoradores Python. Actualice a 0.4.4.[1]

Vector CVSSCVSS: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]
💡 Explicación en Lenguaje Sencillo (ELI5)

Imagina la recepción de un edificio destinada solo a entregas, pero el sistema envía allí a todos. Si solo comprueba quién entró, alguien podría llegar a habitaciones que otro mostrador mantendría cerradas. El SDK afectado ignoraba actions= al registrar un handler de recurso, que podía recibir lecturas, cambios o eliminaciones. El riesgo depende de que el handler no compruebe los permisos necesarios.[1]

Conceptos Clave y Términos

Decorador por recurso
Decorador Python que registra autorización para un tipo de recurso.[1], [2]
Filtro de acción
actions= selecciona las operaciones del handler.[1], [2]
Respaldo
Handler general usado cuando ninguno específico coincide.[1], [2]
CWE-863
Autorización incorrecta: la comprobación no aplica el permiso previsto.[1]

Mapeos de marcos

Análisis de Causa Raíz

En 0.1.45–0.4.3, los tres decoradores afectados aceptaban actions=, pero registraban el handler con el comodín * del recurso. LangGraph podía elegirlo antes que el respaldo general. Si el handler solo autorizaba la acción prevista, podía recibir otras sin esas comprobaciones. El acceso a recursos ajenos requería que la aplicación dependiera de ellas y que el handler no verificara acción y propiedad. 0.4.4 valida la lista y registra cada acción por separado; sin actions=, mantiene el comodín intencionadamente. El aviso asigna CWE-863 y CVSS 4.0 7.6 High. Es un fallo condicionado por la configuración, no un bypass sin autenticación ni un defecto de todas las implementaciones LangGraph. Los decoradores afectados son: @auth.on.threads / @auth.on.assistants / @auth.on.crons. Límite importante de la corrección: el cambio del SDK restringe los registros a las acciones declaradas, pero las rutas de autorización personalizada sin coincidencia siguen permitidas si la aplicación no configura un controlador global de denegación por defecto. El changelog de 0.4.4 indica que langgraph-api 0.10+ advierte al iniciar sobre rutas sin cobertura; considere esa advertencia una señal de detección, no un control de acceso.[1], [2], [5]

Flujo de Ataque Paso a Paso

Paso 1

Configuración

SDK 0.1.45–0.4.3 y decorador afectado con actions=.[1], [4]

Paso 2

Registro

Un handler puede estar destinado a una sola acción.[1]

Paso 3

Comodín

El SDK asocia el handler a todas las acciones del recurso.[1], [2]

Paso 4

Comprobación

La operación llega a un handler que quizá no valida acción y propiedad.[1]

Paso 5

Corrección

Actualice a 0.4.4 y pruebe acción, respaldo y propiedad.[1], [2], [3], [5]

Código Fuente: Vulnerable vs. Seguro

El fragmento vulnerable muestra el comportamiento anterior: _ResourceOn.__call__ usaba * sin importar actions=. La corrección valida y registra cada acción por separado. Es un extracto, no un reemplazo del módulo. Actualice y haga que los handlers comprueben acción y propiedad. Ubicación del código fuente: libs/sdk-py/langgraph_sdk/auth/__init__.py, method _ResourceOn.__call__.[1], [2]

Fragmento de origen: Registro del handler según la acción

IMPLEMENTACIÓN VULNERABLE
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
PARCHE SEGURO Y ROBUSTO
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

Lista de Verificación de Seguridad para Ingeniería

Fuentes