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]

Вектор 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]
💡 Простыми словами (ELI5)

Представьте стойку, которой поручено принимать только доставки, но система направляет к ней всех посетителей. Если проверяется лишь факт входа, посетитель может попасть в комнаты, закрытые другим постом. SDK игнорировал actions= при регистрации обработчика ресурса, который мог получать чтение, изменение и удаление. Риск возникает, если обработчик сам не проверяет права.[1]

Ключевые понятия и термины

Декоратор ресурса
Декоратор Python для авторизации операций типа ресурса.[1], [2]
Фильтр действий
actions= выбирает действия обработчика.[1], [2]
Резервный обработчик
Общий обработчик без подходящего специального.[1], [2]
CWE-863
Неправильная авторизация: проверка не обеспечивает нужные права.[1]

Связи со стандартами

Анализ первопричины

В версиях 0.1.45–0.4.3 декораторы принимали actions=, но регистрировали обработчик по маске ресурса *. LangGraph мог выбрать его раньше общего fallback. Если функция проверяла только ожидаемое действие, другие могли пройти без этих проверок. Доступ к чужим ресурсам требовал зависимости приложения от пропущенных проверок и отсутствия собственной проверки действия и владельца. Версия 0.4.4 проверяет список и регистрирует действия отдельно; без actions= маска намеренно сохраняется. Advisory указывает CWE-863 и CVSS 4.0 7.6 High. Ошибка зависит от конфигурации и не является обходом без аутентификации или дефектом каждой установки LangGraph. Затронуты декораторы: @auth.on.threads / @auth.on.assistants / @auth.on.crons. Важное ограничение исправления: изменение SDK ограничивает регистрацию указанными действиями, но непокрытые пути пользовательской авторизации остаются разрешёнными, если приложение не настроило глобальный обработчик с запретом по умолчанию. В changelog 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 и проверьте действие, fallback и владельца.[1], [2], [3], [5]

Исходный код: Уязвимый vs Защищённый вариант

Уязвимый фрагмент показывает прежнее поведение: _ResourceOn.__call__ использовал * независимо от actions=. Исправление проверяет список и регистрирует каждое действие отдельно. Это фрагмент, не замена модуля. Обновите SDK и проверяйте действие и владельца в обработчиках. Путь к исходному коду: 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

Чек-лист по защите системы для инженеров

Источники