CVE-2026-68771: ComfyUI의 부적절한 직렬화(Insecure Deserialization)를 통한 인증 없는 RCE 취약점
요약
본 기사는 ComfyUI에서 발견된 CVE-2026-68771 취약점을 분석합니다. 이 취약점은 인증 없이 파일 업로드가 가능하고, 내부적으로 안전 플래그가 비활성화된 `torch.load()`를 사용하여 `.pkl` 파일을 역직렬화하는 과정에서 발생합니다. 공격자는 이를 통해 원격 코드 실행(RCE)을 수행할 수 있습니다.
핵심 포인트
- ComfyUI의 LoadTrainingDataset 노드가 취약점의 핵심입니다.
- 안전 플래그(`weights_only=True`)가 비활성화된 `torch.load()` 사용이 위험합니다.
- 공격자는 인증 없이 악성 `.pkl` 파일을 업로드하여 RCE를 유발할 수 있습니다.
개요 (Overview)
| 필드 | 값 |
|---|---|
| CVE ID | CVE-2026-68771 |
| ... | |
ComfyUI는 Stable Diffusion 스타일의 확산 모델(diffusion models)을 실행하기 위한 인기 있는 오픈 소스 노드 그래프 GUI/백엔드입니다. 이 CVE는 내장된 트레이닝 노드인 LoadTrainingDataset에서 발생하며, 해당 노드는 안전 플래그 weights_only=True 없이 디스크의 .pkl 파일을 torch.load()를 사용하여 역직렬화(deserializes)합니다. 이것이 단순히 '이론적으로 위험한' 수준을 넘어 심각한 이유는 공격자가 어떠한 인증도 없이 해당 .pkl 파일을 업로드할 수 있기 때문입니다. |
다시 말해, 이것은 단일 버그가 아니라 두 개의 독립적인 결함의 사슬(chain)입니다:
- 콘텐츠나 확장자 검증을 전혀 수행하지 않는 파일 업로드 엔드포인트.
- 이 파일을 내부적으로
pickle로 역직렬화하며, 안전 플래그가 활성화되지 않은 트레이닝 데이터셋 로더.
두 가지를 모두 분석해 보겠습니다.
배경: 왜 torch.load()가 위험할 수 있는가 (Background: why torch.load() can be dangerous)
PyTorch의 torch.load()는 내부적으로 Python의 pickle 모듈을 사용합니다. pickle은 단순한 데이터 형식이 아니라, 역직렬화 과정에서 객체를 어떻게 재구성할지에 대한 명령을 실행 가능한 opcode 스트림으로 인코딩합니다.
이러한 opcode 중 하나인 REDUCE는 unpickling 중에 이를 만날 때마다 (호출 가능한 객체(callable), 인수(args)) 튜플을 직접 호출합니다. 따라서 다음과 같은 객체를 pickle 파일에 심으면:
import pickle, os
class Payload:
...
이 파일을 가지고 단순히 pickle.load() (또는 torch.load())를 호출하는 것만으로도 os.system(...)가 트리거됩니다. 파일
이를 완화하기 위해, 최근 버전의 PyTorch는 torch.load(f, weights_only=True)를 지원합니다. 이 플래그가 활성화되면 텐서(tensors), 숫자(numbers), 기본 컨테이너 등 "안전한" 타입만 재구성할 수 있도록 허용되며, 임의의 Python 객체(특히 __reduce__를 가진 모든 객체)는 거부됩니다. 문제는 다음과 같습니다: **PyTorch 버전이 2.6보다 오래된 경우, 이 플래그의 기본값은 False**이며, ComfyUI의 노드 기능은 바로 그 허점(gap)에 걸렸습니다.
소스 코드 분석 ①: 취약한 로더 — comfy_extras/nodes_dataset.py
다음은 패치 전 코드의 관련 부분입니다.
class LoadTrainingDataset(io.ComfyNode):
"""Load encoded training dataset from disk."""
...
라인별로 살펴보겠습니다:
folder_name은 사용자(= 잠재적 공격자)가 ComfyUI 워크플로우 그래프를 통해 직접 제공하는 노드 매개변수입니다. 기본값은"training_dataset"이지만, 어떤 문자열이든 전달될 수 있습니다.os.listdir(dataset_dir)는 해당 폴더에서shard_*.pkl패턴과 일치하는 모든 파일을 스캔하며 — 누가 이 파일을 작성했는지 또는 언제 작성했는지에 대한 어떠한 검사도 수행하지 않습니다.- 결정적인 라인:
shard_data = torch.load(f). 여기에weights_only=True가 없습니다. 이는 예상되는 잠재값(tensors)과 조건화 값(tensors + dicts)을 넘어 어떤 Python 객체든 재구성될 수 있음을 의미합니다. 만약 파일에 위 예시의Payload클래스처럼__reduce__를 가진 객체가 포함되어 있다면, 이 라인에서 임의 코드가 실행됩니다.
이 스니펫만 읽어도 명백한 질문이 떠오릅니다: "물론이지만, 공격자가 여전히 악성 .pkl 파일을 서버 디스크에 올릴 방법이 필요하지 않을까요?" 그것이 바로 두 번째 결함입니다.
소스 코드 분석 ②: 검증되지 않은 업로드 — server.py의 /upload/image
def image_upload(post, image_save_function=None):
image = post.get("image")
overwrite = post.get("overwrite")
...
경로와 함수 이름이 각각 /upload/image / image_upload임에도 불구하고, 여기서는 업로드된 콘텐츠가 실제로 이미지는 아님을 아무도 확인하지 않습니다.
get_dir_by_type("output")을 호출하면 업로드 대상이 ComfyUI의output디렉터리로 결정됩니다. 공격자는 요청에서type=output만 설정하면 됩니다.subfolder는 완전히 공격자가 제어할 수 있는 값이며, 이는 나중에LoadTrainingDataset에서folder_name과 일치하는 데 사용되는 바로 그 값입니다.filename역시 그대로 사용됩니다. 공격자는 단지LoadTrainingDataset이 찾고 있는 패턴—즉,shard_접두사와.pkl확장자(예:shard_0001.pkl)—만 맞추면 됩니다.- 존재하는 유일한 보안 검사는
os.path.commonpath비교이며, 이는 출력 디렉터리 탈출 (경로 순회)을 방지합니다. 하지만 이것은 무엇이 업로드되는지와는 아무 관련이 없습니다. 확장자 화이트리스트도 없고, MIME-type 검사도 없으며, 파일 시그니처(매직 바이트) 검사도 없습니다. - 그리고 이 경로는 단순한
@routes.post("/upload/image")로 등록되어 있어 인증 미들웨어(authentication middleware)가 앞에 붙어 있지 않습니다. 만약 ComfyUI 인스턴스가 기본 설정(인증 없음)으로 네트워크에 노출된다면, 누구나 이것을 호출할 수 있습니다.
결과적으로: 이름에 관계없이 이 엔드포인트는 범용적인 임의 파일 쓰기 원시 기능(arbitrary-file-write primitive)입니다. 즉, 어떤 이름이든 상관없이 모든 바이트를 공격자가 선택한 출력 디렉터리의 하위 폴더에 쓸 수 있습니다.
두 취약점이 만나는 지점
각 취약점은 그 자체로
- 공격자는 악의적인
__reduce__객체를 포함하는 pickle 페이로드를 제작하여, 이를shard_0001.pkl이라는 이름으로 지정하고,type=output및subfolder=<임의 폴더>옵션과 함께/upload/image에 업로드합니다. 인증이 필요 없습니다. - 서버는 이 파일을 검증 없이
output/<임의 폴더>/shard_0001.pkl에 작성합니다. - 공격자는 1단계에서 사용된 것과 동일한
<임의 폴더>를folder_name으로 설정하는LoadTrainingDataset노드를 포함하여/prompt를 통해 워크플로우를 제출합니다. 이 역시 인증이 필요 없습니다. - 실행 과정 중, 해당 노드는 그 폴더 내에서
shard_*.pkl파일을 검색하고 이를torch.load(f)로 엽니다.weights_only=True가 없기 때문에, pickle 스트림의REDUCEopcode가 그대로 실행됩니다. - 공격자가 준비한
(callable, args)튜플이 호출되어 ComfyUI 프로세스의 권한으로 임의 명령을 실행합니다.
업로드부터 실행까지 모든 단계에 인증이 필요하지 않기 때문에, 네트워크로 접근 가능한 모든 ComfyUI 인스턴스가 악용될 수 있습니다.
해결책 (The fix)
공식 수정 사항은 PR #14543 커밋 94ee49b에서 적용되었으며, 변경 내용은 단 한 줄입니다:
with open(shard_path, "rb") as f:
- shard_data = torch.load(f)
+ shard_data = torch.load(f, weights_only=True)
PR 설명에 따르면, 이 부분은 전체 코드베이스에서 weights_only=True가 누락된 유일한 torch.load() 호출이었습니다 (다른 호출 지점인 comfy/utils.py와 comfy/sd1_clip.py에서는 이미 설정되어 있었습니다). weights_only=True를 활성화하면, pickle 복원 시 텐서(tensors), 숫자(numbers), 기본적인 리스트/딕셔너리 컨테이너 등 안전한 타입만 허용하고, __reduce__를 사용한 임의 객체 재구성은 아예 거부합니다. 즉, 4단계에서 REDUCE opcode가 실행될 기회조차 주지 않고 역직렬화 자체가 차단됩니다.
참고로, 이 패치는 첫 번째 결함(업로드 유효성 검사 부족)은 건드리지 않습니다. /upload/image는 여전히 임의 파일을 출력 디렉터리에 쓸 수 있습니다. 변경된 점은 이러한 파일이 더 이상 LoadTrainingDataset을 통해 코드 실행으로 이어질 수 없다는 것입니다. 이 수정 사항은 체인의 마지막 고리(직렬화)를 닫았을 뿐, 진입점(업로드) 자체는 아닙니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
