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 데코레이터의 action 필터를 무시했습니다. 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]
Action 필터
actions=가 처리할 작업을 선택합니다.[1], [2]
대체 핸들러
전용 일치가 없을 때 사용하는 일반 핸들러입니다.[1], [2]
CWE-863
잘못된 권한 부여: 검사가 의도한 권한을 적용하지 못합니다.[1]

프레임워크 매핑

근본 원인 분석 (Root Cause)

0.1.45–0.4.3은 actions=를 받으면서 핸들러를 리소스 와일드카드 *에 등록했습니다. LangGraph는 일반 대체 핸들러보다 먼저 선택할 수 있었습니다. 함수가 예정된 작업만 허가하면 다른 작업도 대체 검사 없이 전달될 수 있습니다. 앱이 생략된 검사에 의존하고 핸들러가 작업과 소유권을 확인하지 않을 때에만 타인 리소스 접근이 가능합니다. 0.4.4는 목록을 검증하고 작업별로 등록하며, 인수 생략 시 와일드카드를 유지합니다. 권고는 CWE-863, CVSS 4.0 7.6 High입니다. 설정에 따른 권한 결함이며 인증 없는 우회나 모든 배포의 문제는 아닙니다. 영향받는 데코레이터는 다음과 같습니다: @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]

소스 코드 비교: 취약한 구현 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

엔지니어링 및 시스템 보안 강화 체크리스트

출처