상태 변화와 처리 순서가 만든 보안 문제

security

9 분 읽기

SERIES OpenAI Hugging Face 해킹 사건 8 / 8 시리즈 전체 보기 →

TOCTOU(Time Of Check To Time Of Use) 의 개념은 단순하고 명확하다. 사용하려는 어떤 자원의 상태를 확인한 시점과 실제로 사용하는 시점 사이에는 시간 차이가 존재하며, 그 사이 대상의 상태가 달라질 수 있다는 것이다.

이번 글에서는 TOCTOU가 무엇인지부터 시작해 실제 사례를 살펴보도록 하겠다. 또한 Race Condition과의 관계를 살펴보고, 간단한 실습을 통해서 직접 동작을 확인해보려 한다. 마지막으로, OpenAI HuggingFace 해킹 사건에서 TOCTOU가 어떤 형태로 등장했는지 살펴보자.

TOCTOU란 무엇일까

TOCTOU(Time Of Check To Time Of Use)는 자원의 상태를 확인한 시점(Check)과 실제로 사용하는 시점(Use) 사이에 상태가 변경되어, 검사 결과와 실제 사용 상태가 달라지는 문제를 말한다.

프로그램은 보통 작업 전에 대상의 상태를 확인하곤 한다. 예를 들어 파일이 존재하는지 여부, 접근 권한, 경로의 안전성, 데이터의 현재 값 등을 검사한 뒤 실제 작업을 수행하는 것이다. 문제는 확인(check)과 작업(use) 두 단계가 하나의 원자적 연산이 아닐 경우, 그 사이에 다른 프로세스나 악의적인 공격자의 개입에 의해 대상의 상태를 바꿀 수 있다는 점이다.

sequenceDiagram
    participant Program
    participant Resource
    participant Attacker as Other Process / Attacker

    Program->>Resource: Check
    Resource-->>Program: 현재 상태는 안전함

    Attacker->>Resource: 상태 변경

    Program->>Resource: Use
    Resource-->>Program: 변경된 상태의 자원 사용

TOCTOU가 문제가 되는 경우의 핵심은 Check의 결과가 Use 시점에도 여전히 유효하다고 가정하지만, 실제로는 그 상태가 보장되지 않을 수 있다는 것이다.

즉, TOCTOU는 다음 조건이 함께 존재할 때 성립한다.

조건 설명
Check와 Use가 분리되어 있음 검사와 실제 사용이 서로 다른 시점에 수행됨
대상 상태가 변경 가능함 두 시점 사이에 파일·권한·값·참조 대상 등이 바뀔 수 있음
변경을 통제하지 못함 다른 프로세스·스레드·사용자 등이 중간에 개입할 수 있음
기존 Check 결과를 그대로 신뢰함 Use 시점에 상태가 동일한지 보장하거나 다시 검증하지 않음

따라서 TOCTOU는 특정 파일 시스템 취약점만을 의미하는 게 아니다. 파일, 권한, 데이터베이스 값, 네트워크 주소처럼 검사 이후 상태가 변경될 수 있는 자원을 대상으로 Check와 Use가 분리되어 있다면 구조적으로 TOCTOU의 문제가 발생할 수 있다.

TOCTOU의 유형

대표적인 유형을 몇 가지 살펴보자.

유형 Check Use 불일치를 유발하는 사건
파일 접근 파일의 권한·상태 확인 파일 열기·수정 파일 또는 경로가 다른 대상으로 변경
임시 파일 생성 파일 존재 여부 확인 파일 생성·쓰기 다른 프로세스가 같은 경로를 선점
데이터베이스 데이터 존재 여부 확인 데이터 추가 다른 요청이 먼저 데이터 생성
권한 검사 사용자 권한 확인 보호된 작업 수행 Check 이후 권한 또는 대상 상태 변경
네트워크 요청 URL·IP가 안전한지 확인 실제 요청 전송 DNS 응답이나 대상 주소 변경

1. 파일 권한 확인 후 파일 열기

첫 번째로 살펴볼 유형은 대표적인 TOCTOU 유형으로 꼽히는 파일 접근의 경우이다. 코드 파일 하나를 예시로 들어보자.

1
2
3
4
5
6
7
8
9
void write_file(const char *text) {
  if (access("/tmp/data", W_OK) == 0) {
      int fd = open("/tmp/data", O_WRONLY | O_APPEND);
      if (fd != -1) {
          write(fd, text, strlen(text));
          close(fd);
      }
  }
}

access()는 현재 사용자가 /tmp/data에 쓸 수 있는지 확인하고, open()은 실제로 해당 경로의 파일을 연다. 문제는 두 함수가 서로 독립된 시스템 호출이라는 점이다. 즉, 시간차에 의해 두 함수가 각각 검사한 파일과 실제로 연 파일이 달라질 수 있다.

파일에 대한 TOCTOU를 악용하는 한 가지 공격 시나리오를 가정해보자.

  • 높은 권한을 가진 프로그램이 사용자가 입력한 내용을 /tmp/data에 기록하는 함수가 존재함
  • 공격자는 이 서버의 SSH 로그인을 해 서버 내 자원을 탈취하고자 함.
  • 이를 위해 /root/.ssh/authorized_keys 에 접근해서 자신의 SSH Key를 추가하고자 함
  • 일반 사용자 권한으로는 원래 /root/.ssh/authorized_keys를 수정하지 못함

이 때, 공격자가 write_file 함수에 자신의 SSH Key를 입력으로 보내면서, accessopen 사이에 /tmp/data/root/.ssh/authorized_keys 파일을 가리키는 심볼릭 링크로 교체한다면, 공격자의 의도대로 SSH Key가 authorized_keys 파일에 쓰여질 것이다.

시점 동작 /tmp/data 파일
Check /tmp/data에 접근 가능한지 확인 /tmp/data
State Change 공격자가 /tmp/data가 가리키는 대상을 변경 authorized_keys
Use /tmp/data를 다시 열어 사용 authorized_keys

이해를 돕기 위한 흐름도는 다음과 같다.

이 취약점은 이후 CVE-2026-65617로 NIST의 vulnerability DB에 Artifactory의 Package Handling 과정에서 발생하는 역직렬화 취약점으로 등록됐다. 즉, TOCTOU는 OpenAI 에서 이 공격의 처리 순서 설명 중 이해를 돕기 위해 말한 것이며, 실제 취약점은 검증되지 않은 Ruby 객체의 역직렬화 라고 해야 정확할 것 같다.

이번 해킹 사례는 전형적인 TOCTOU와 다르기는 하나, 프로그램이 어떤 대상을 검증한 시점과 실제로 사용하는 시점의 원자성이 보장되지 않았다는 점에서 공통적이다.

sequenceDiagram
  participant Attacker
  participant Program
  participant file_a as /tmp/data
  participant authorized_keys

  Attacker ->> Program: write_file(SSH_KEY)
  Program ->> file_a: access(W_OK)
  file_a -->> Program: Writable
  
  Attacker ->> file_a: authorized_keys 를 가리키는 심볼릭 링크로 교체

  Program ->> file_a: open()
  file_a ->> authorized_keys: Symbolic Link

  authorized_keys -->> Program: File Descriptor
  Program ->> authorized_keys: write(SSH_KEY)

그런데 이러한 TOCTOU는 실제 애플리케이션 개발 중에도 “악의 없이” 흔히 일어나는 실수이기도 하다.


2. 권한 검사 후 작업 수행

두 번째 유형은 사용자의 권한을 먼저 확인한 뒤 실제 작업을 수행하는 경우다. 예를 들어 다음과 같이 관리 권한을 확인한 뒤 작업을 실행한다고 해보자.

1
2
3
def execute_admin_task(user, task):
    if user.has_role("admin"):
        queue.add(task)

프로그램은 요청 시점에 사용자가 관리자 권한을 가지고 있는지 확인한다. 문제는 권한을 확인한 시점과 실제 작업이 수행되는 시점이 다를 수 있다는 것이다.

비슷하게 한 가지 상황을 가정해보자.

  • 사용자가 관리자 권한을 가진 상태에서 중요한 작업을 요청함
  • 프로그램은 권한을 확인한 뒤 해당 작업을 실행 대기열에 등록함
  • 작업이 실제로 실행되기 전에 사용자의 관리자 권한이 회수됨
  • 하지만 실행 단계에서는 권한을 다시 확인하지 않음

이 경우 실제 작업이 수행되는 시점에는 사용자가 더 이상 관리자 권한을 가지고 있지 않지만, 프로그램은 이전의 검사 결과를 그대로 신뢰하고 작업을 수행한다.

시점 동작 사용자 권한
Check 관리자 권한 확인 Admin
State Change 사용자의 관리자 권한 회수 User
Use 이전 검사 결과를 바탕으로 작업 수행 User


3. 그래서 TOC TOU는

앞에서 살펴본 유형들은 대상과 상황은 서로 다르지만 구조는 동일하다. 파일 접근에서는 검사한 파일과 실제로 연 파일이 달라졌고, 권한 검사에서는 확인 당시의 권한과 작업 수행 당시의 권한이 달라졌다. 결국 문제는 모두 과거에 확인한 상태가 현재도 그대로 유지되고 있다고 가정한 것에서 발생한다.

Check와 Use가 분리되어 있고, 그 사이 상태가 변할 수 있는데도 이전의 Check 결과를 그대로 신뢰하는 것.

이것이 TOCTOU의 본질이다.

Race Condition

Race Condition(경쟁 상태)

TOCTOU는 Race Condition과 함께 다루어지는 경우가 많다. Race Condition은 여러 실행 흐름이 같은 자원이나 상태에 접근할 때, 실행 순서나 타이밍에 따라 결과가 달라지는 문제이다.

예를 들어 두 요청이 거의 동시에 같은 데이터를 확인하고 수정한다면, 어느 요청이 먼저 실행되느냐에 따라 최종 결과가 달라질 수 있다. 즉, 여러 작업의 실행 순서가 결과에 영향을 준다는 것이다.

TOCTOU 역시 이러한 Race Condition의 한 형태다. 다만 Race Condition은 여러 유형으로 나타날 수 있지만, TOCTOU는 경쟁이 발생하는 지점이 Check와 Use 사이로 명확하다.

1
2
3
4
5
Check
  ↓
Race Window
  ↓
Use

Race Window

이와 같은 Race Condition이 실제로 발생할 수 있는 시간 구간을 Race Window라고 한다. 이 구간에 다른 프로세스나 스레드, 요청 또는 공격자가 개입해 상태를 변경하면 그 개입의 선후에 따라 결과가 달라질 수 있다.

앞에서 살펴본 파일 접근 사례를 다시 보면 관계가 명확하다.

TOCTOU와 Race Condition

둘의 관계를 정리하면 다음과 같다.

구분 Race Condition TOCTOU
핵심 실행 순서와 타이밍에 따라 결과가 달라짐 Check와 Use 사이의 상태 변화에 따라 결과가 달라짐
범위 동시성 문제 전반 Race Condition의 특정 형태
경쟁 지점 공유 자원에 접근하는 모든 구간 Check와 Use 사이의 Race Window

즉, TOCTOU는 Race Condition의 한 종류이다.

OpenAI Hugging Face 사건에서의 TOCTOU

이제 다시 OpenAI Hugging Face 해킹 사건으로 돌아가보자. 이 사건에서는 앞서 살펴본 파일이나 권한 검사와는 다른 형태의 TOCTOU가 등장한다.

이 사건에서 TOCTOU는 7월 13일, 에이전트들이 OpenAI 내부 Artifactory를 다시 장악하는 과정에서 사용됐다. 먼저 에이전트들은 이전에 확보한 SSRF 방법을 이용해 외부 인터넷의 어느 저장소에 조작된 Ruby 패키지(편의상 악성패키지라고 지칭) 를 올려놓았다. 그리고 내부에서는 Artifactory에 “나 그 악성 패키지 필요해!”라고 요청하여 Artifactory가 그 패키지를 받아오도록 유도했다.

그렇다면 에이전트들은 조작된 Ruby 패키지로 무엇을 하려고 한 걸까? 문제는 이 Ruby 패키지를 처리하는 순서에 있었다.

일반적으로 외부에서 전달된 객체를 처리한다면, 먼저 해당 객체가 안전한지 검사한 뒤 실제로 역직렬화하거나 사용해야 한다.

flowchart LR
  A(Safety Check) --> B(Deserialization)
  B --> C(Use)

하지만 Artifactory의 JRuby 기반 RubyGems 처리 경로에서는 Ruby Package 내부의 Nested Child와 Dependency가 안전한지 확인(Safety Check)되기 전에 Sandbox 밖에서 먼저 역직렬화가 될 수 있는 취약점이 존재했다. OpenAI 기술 보고서에서는 이를 다음 순서로 설명한다.

flowchart LR
  A(조작된 Ruby 패키지) --> B(Nested Child / Depndency)
  B --> C(Sandbox 밖에서 역직렬화)
  C --> D(안전 확인 Safety Check. 뒤늦게 이뤄짐)

즉, 안전한지 확인해야 할 데이터가 검사되기도 전에 이미 사용된 것이다. 살펴보면 전형적인 TOCTOU와 구조가 조금 다름을 알 수 있다. 하지만 OpenAI에서는 개념상 Check와 Use가 보장되지 않았다는 점에서 TOCTOU로 설명한 것이다.

앞에서 살펴본 파일 TOCTOU와 비교하면 차이가 더 명확하다.

구분 전형적인 TOCTOU Artifactory 사례
Check 대상의 상태·권한 확인 Ruby Package의 안전성 확인
문제 Check 이후 대상의 상태가 변경됨 Check가 끝나기 전에 Dependency가 처리됨
Use 변경된 대상을 사용 Nested Dependency를 Unsandboxed Deserialization
결과 검사한 대상과 실제 사용 대상 불일치 검증되지 않은 객체가 먼저 실행 가능

따라서 이번 사례를 단순히 Check → State Change → Use로 설명하기는 어렵다. 공격자가 Check와 Use 사이에서 객체를 교체한 것이 아니라, Artifactory 내부의 처리 순서 자체가 검증보다 사용을 먼저 허용하고 있었기 때문이다.

에이전트들은 이 취약점을 이용해 Artifactory에서 Remote Code Execution(RCE)을 확보했다. 이후 이것을 발판삼아 Artifactory host 탈취에 이어 전반적인 훈련 K8s 권한 획득까지 이어지게 된다.

CVE-2026-65617

이 취약점은 이후 CVE-2026-65617로 NIST의 vulnerability DB에 Artifactory의 Package Handling 과정에서 발생하는 역직렬화 취약점이라는 내용으로 등록됐다. OpenAI에서는 이 취약점을 TOCTOU라고 설명했지만, 이는 공격의 처리 순서 설명 중에 이해를 돕기 위해 말한 것으로 보이며, 실제 취약점은 검증되지 않은 Ruby 객체의 역직렬화 라고 해야 정확할 것 같다.

전형적인 TOCTOU와 다르기는 하나, 프로그램이 어떤 대상을 검증한 시점과 실제로 사용하는 시점의 원자성이 보장되지 않았다는 점에서는 공통점이 있다.

TOCTOU 실습

이번에는 간단한 Python 코드로 Check와 Use 사이에 상태가 변경되는 상황을 직접 만들어보도록 하겠다. 실제 공격 상황을 구현하기에는.. 아이디어가 없어서, 우선 멀티스레딩 환경에서 TOCTOU가 어떤 구조로 발생하는지 확인해보도록 한다.

아래 코드에서 program 함수는 target.txt 파일의 안전성을 체크한 뒤, 잠시 뒤 그 파일의 텍스트를 읽어와 그 내용을 표준 출력으로 출력한다. another_process 함수는 동일한 파일에 “악성코드”라는 텍스트를 쓰는 작업을 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pathlib import Path
import threading

target = Path("target.txt")
target.write_text("SAFE")

checked = threading.Event()
changed = threading.Event()


def program():
    # Check
    if target.read_text() == "SAFE":
        print("[PROGRAM] Check - SAFE 확인")

        checked.set()
        changed.wait()

        # Use
        data = target.read_text()
        print(f"[PROGRAM] USE - {data} 사용")


def another_process():
    checked.wait()

    target.write_text("악성코드")
    print("[Attacker] Change - 파일 내용 변경")

    changed.set()


t1 = threading.Thread(target=program)
t2 = threading.Thread(target=another_process)

t1.start()
t2.start()

t1.join()
t2.join()

실행 결과는 다음과 같다.

1
2
3
[PROGRAM] Check - SAFE 확인
[Attacker] Change - 파일 내용 변경
[PROGRAM] USE - 악성코드 사용

프로그램은 분명 Check 단계에서 SAFE라는 값을 확인했다. 하지만 실제 Use 단계에에 들어가기 전, 다른 작업이 파일을 변경했기 때문에 원래 확인했던 파일 내용이아닌 악성코드를 사용하게 된다.

시점 동작 target.txt
Check 파일 내용 확인 SAFE
State Change 다른 스레드가 파일 변경 악성코드
Use 파일을 다시 읽어 사용 악성코드

threading.Event()는 TOCTOU 시점을 일부러 맞추기 위해 사용함. 실제 환경에서는 이렇게 인위적인 이유 때문이 아니라, 파일 I/O, 네트워크 상황, DB 작업 동시 요청 등으로 인해 자연스럽게 Race Window가 만들어진다.

방지 방법

가장 효가적인 방법은 Check와 Use를 가능한 한 분리하지 않는 것이다. DB에서 자주 볼 수 있는 용어를 가져와 표현해 보면 Check와 Use를 하나의 트랜잭션으로 묶어 원자성을 보장하는 것이다. 예를 들어 파일의 존재 여부를 먼저 확인한 뒤 다시 생성하는 대신, 파일 생성 자체를 원자적인 연산으로 처리할 수 있다.

1
2
with open("result.txt", "x") as f:
    f.write("data")

"x" 모드는 파일이 이미 존재하면 실패하고, 존재하지 않으면 생성한다. 즉, 상태 확인과 사용을 별도로 수행하지 않는다.

결국 TOCTOU를 방지하는 핵심은 단순하다.

확인한 뒤 사용하는 것이 아니라, 가능하면 확인과 사용을 하나의 원자적인 작업으로 처리해야 한다.

Reference

Wikipedia - TOCTOU의 기본 개념과 파일 시스템 사례

Black Hat USA 2026 OpenAI HuggingFace 해킹 사건 발표

OpenAI Technical Report - Artifactory 재장악과 JRuby/RubyGems 공격 과정

NVD CVE-2026-65617 - Artifactory Deserialization 취약점 분류

MITRE CWE-367 - TOCTOU의 정의와 Race Condition 관계

MITRE CWE-362 - Race Condition의 정의와 동시성 문제

// reading compass

이 글과 이어지는 경로

시리즈, 카테고리, 태그 겹침, 최신도를 점수화해 가까운 글일수록 중심에 배치합니다.

hover nodes or cards · click a node to pin
Next reads 0

    댓글 남기기