FlawOpen

flawopen.com/Teardowns/cve-2026-84973-jupyterlab-snapshot-checkout-toctou

● CVE-2026-84973 · 높음
보안 연구 · FlawOpen

CVE-2026-84973: JupyterLab 스냅샷 워크플로의 TOCTOU 취약점

초 단위 시각 검증으로 인해 스냅샷 워크플로가 승인되지 않은 Pull Request 커밋을 체크아웃해 작업 권한으로 실행할 수 있었습니다. update-snapshots-checkout을 1.0.0 이상으로 업데이트하세요.

💡 알기 쉬운 설명 (ELI5)

검토자가 특정 소포를 승인했지만, 배송 창구는 시계의 초만 확인합니다. 같은 초에 소포를 바꾸면 바뀐 소포도 승인된 것처럼 보입니다. 이 action은 초 단위 시각을 비교하면서 같은 값도 허용했기 때문에 관리자 댓글과 동시에 Pull Request 업데이트가 검사를 통과할 수 있었습니다.

핵심 개념 및 용어

TOCTOU
Time-of-check to time-of-use: 보안 결정을 내린 리소스가 실제 사용 전에 바뀌는 문제입니다.
Pull Request head SHA
Pull Request 브랜치 끝의 커밋을 가리키는 불변 식별자입니다. 워크플로는 승인된 정확한 커밋만 실행해야 합니다.
GitHub Actions 토큰 권한
작업의 GITHUB_TOKEN이 수행할 수 있는 저장소 작업으로, 콘텐츠와 Pull Request 읽기 또는 쓰기가 포함됩니다.
CWE-367 / CWE-829
보안 권고는 시간 경쟁을 CWE-367로, Pull Request가 제어하는 기능 실행을 CWE-829로 분류합니다.

프레임워크 매핑

  • 이 가이드에서 설명하는 약점의 CWE 참조: CWE-367 CWE-829

근본 원인 분석 (Root Cause)

1.0.0 이전의 update-snapshots-checkout은 Pull Request의 pushed_at과 트리거 댓글의 created_at을 가져와 Unix 초로 변환한 뒤 pushed_at이 엄격히 더 큰 경우에만 실행을 거부했습니다. 두 API 시각은 초 단위 정밀도라 같은 초에 발생한 push와 승인 댓글은 같은 값이 되어 검사를 통과할 수 있었습니다. 이어서 action은 현재 head SHA를 저장하고 checkout 뒤에 비교했지만, 이는 checkout이 방금 조회한 SHA와 일치함만 확인할 뿐 관리자가 그 커밋을 승인했음을 증명하지 않습니다. upstream 수정은 -gt를 -ge로 바꿔 같은 시각과 이후 push를 거부합니다. 취약한 action을 사용하고 이후 단계가 Pull Request 내용을 실행하는 워크플로가 영향을 받으며, 피해 범위는 워크플로 권한과 자격 증명에 따라 달라집니다.

단계별 공격 실행 흐름

단계 1

신뢰된 관리자가 워크플로를 실행

Pull Request가 생성되고 OWNER, COLLABORATOR 또는 MEMBER가 스냅샷 업데이트 워크플로를 시작하는 댓글을 게시합니다.

단계 2

공격자가 관리하는 업데이트가 댓글과 같은 초에 발생

기여자가 같은 초에 Pull Request 브랜치를 갱신합니다. API가 초 단위 정밀도만 제공하므로 Unix 초 값이 같을 수 있습니다.

단계 3

엄격한 초과 비교가 같은 값을 허용

취약한 action은 pushed_at이 created_at보다 클 때만 거부합니다. 같은 값은 조건을 만족하지 않아 새 head SHA가 저장됩니다.

단계 4

워크플로가 해당 커밋을 체크아웃하고 실행할 수 있음

checkout은 저장된 SHA와 일치해 후속 검사를 통과합니다. 이후 단계가 Pull Request가 제어하는 action이나 스크립트를 실행하면 작업 권한과 자격 증명이 사용됩니다.

단계 5

업데이트하고 워크플로 권한을 축소

1.0.0 이상으로 업데이트하세요. action을 불변 SHA로 고정하고 GITHUB_TOKEN에 필요한 최소 권한만 부여하며, 신뢰할 수 없는 Pull Request 코드와 비밀 정보 및 권한 높은 작업을 분리하세요.

소스 코드 비교: 취약한 구현 vs 보안 패치

upstream의 한 줄 수정과 동일합니다. -ge는 API 시각 정밀도에서 같은 값도 거부하므로 문서화된 같은 초 우회를 막습니다. 그래도 승인은 불변 SHA에 연결하고 권한을 최소화해야 합니다. 시간 순서는 불변 승인 결정을 대체하지 못합니다. upstream diff는 기존 진단 메시지를 그대로 둡니다. 메시지에는 “이후”라고 되어 있지만 수정 후에는 같은 시각도 거부됩니다.

취약한 구현
if [[ $(date -d "$pushed_at" +%s) -gt $(date -d "$COMMENT_AT" +%s) ]]; then
    echo "Updating is not allowed because the PR was pushed to (at $pushed_at) after the triggering comment was issued (at $COMMENT_AT)"
    exit 1
fi
보안 강화 패치
if [[ $(date -d "$pushed_at" +%s) -ge $(date -d "$COMMENT_AT" +%s) ]]; then
    echo "Updating is not allowed because the PR was pushed to (at $pushed_at) after the triggering comment was issued (at $COMMENT_AT)"
    exit 1
fi

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

출처