FlawOpen

flawopen.com/Incidents/gitlab-commits-api-path-traversal-september-2026

● GitLab · CVE-2026-85706 · September 2026 · CVSS 10.0 · 심각
보안 연구 · FlawOpen

CVE-2026-85706: GitLab 서버 파일 노출

GitLab의 인증 없는 업로드 분석은 서버 파일을 선택할 수 있었습니다. 잘못된 이스케이프는 오류에 일부 내용을 드러냈지만, 선택만으로 파일이 공개되지는 않았습니다.[1], [3]

CVSS 벡터CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N[3]
💡 알기 쉬운 설명 (ELI5)

접수 담당자가 받침대에 놓인 서류 대신 방문자가 적어 낸 보관함 위치를 따른다고 생각해 보세요. 신분증을 확인하기 전에 보관함을 열고, 읽을 수 없는 줄이 있으면 오류를 설명하며 그 내용을 소리 내어 읽습니다. 서류를 여는 것과 내용을 되풀이하는 것은 서로 다른 두 잘못입니다.[1]

핵심 개념 및 용어

Upload metadata
요청에서 온 file.path 또는 metadata.path가 신뢰하는 업로드 처리기가 등록한 버퍼를 대체해서는 안 됩니다.[1]
Percent decoding
양식 분석은 % 뒤의 두 자리 16진수를 해독합니다. 잘못된 순서는 문제 부분이 포함된 오류를 낼 수 있습니다.[1]
CWE-22 / CVSS
CNA는 CVE-2026-85706을 경로 탐색, CVSS 10.0 치명적으로 분류했습니다. 요청자가 서버 파일을 선택한 것이 깨진 경계입니다.[3]

프레임워크 매핑

사고 타임라인

2026-09-10

GitLab이 수정 버전 19.1.8, 19.2.6, 19.3.2를 배포했습니다.[2]

2026-09-23

공지에 이전 계열 수정 버전 18.11.12와 19.0.9가 추가됐습니다.[2]

단계별 공격 실행 흐름

단계 1

인증 이전 선택

업로드 메타데이터가 인증 전에 예상한 임시 버퍼를 서버 경로로 바꿀 수 있었습니다.[1]

단계 2

파일을 양식으로 해석

application/x-www-form-urlencoded에서 선택된 파일은 정상 커밋 응답이 아닌 양식 데이터로 해석됐습니다.[1]

단계 3

오류에 비친 내용

잘못된 퍼센트 이스케이프가 해당 부분을 오류에 담을 수 있었습니다. & 와 = 가 범위를 결정하며, 읽기만으로 유출이 증명되지는 않습니다.[1]

단계 4

조사

correlation_id로 Rails API와 Workhorse 항목을 연결하고 api_error와 written_bytes를 비교해 응답 바이트가 전송되었는지 판단하세요.[1]

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

이 Python 코드는 교육용 모델이며 GitLab 원본이나 실제 패치가 아닙니다. 취약 예제는 메모리 버퍼만 사용하고 모든 %를 오류로 간주하므로 실제 검증이나 부분 선택을 재현하지 않습니다. 개선 예제는 먼저 인증하고 경로 메타데이터를 거부하며 신뢰하는 업로드 시스템의 바이트만 받아 크기를 제한하고 일반 오류를 돌려줍니다. 공급자 업데이트도 적용하세요. 앞단에서 파일을 이미 읽었다면 경로 안에서 인증하는 것은 늦습니다.[1]

취약한 구현
from urllib.parse import unquote_to_bytes

def parse_upload(authenticated, metadata, buffers):
    # 안전한 메모리 내 fixture이며 파일 시스템에 접근하지 않습니다.
    body = buffers.get(metadata.get("path"), b"")
    if b"%" in body:
        return 400, body.decode("ascii", errors="replace")
    if not authenticated:
        return 401, "Unauthorized"
    return 200, unquote_to_bytes(body)
보안 강화 패치
from urllib.parse import unquote_to_bytes

MAX_BYTES = 4096
HEX = frozenset(b"0123456789abcdefABCDEF")

def parse_upload(authenticated, metadata, registered_body):
    # registered_body는 신뢰할 수 있는 업로드 미들웨어에서 오며,
    # 요청 매개변수로 선택한 경로에서 오지 않습니다.
    if not authenticated:
        return 401, "Unauthorized"
    if metadata or type(registered_body) is not bytes:
        return 400, "Invalid upload"
    if len(registered_body) > MAX_BYTES:
        return 413, "Upload too large"
    for i, byte in enumerate(registered_body):
        if byte == 37 and (i + 2 >= len(registered_body) or
                registered_body[i+1] not in HEX or
                registered_body[i+2] not in HEX):
            return 400, "Invalid upload"
    return 200, unquote_to_bytes(registered_body)

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

출처