포스트

GitLab CVE-2026-85706 야생 악용: 외부 노출 인스턴스 운영 런북

Commits API Path Traversal(CVE-2026-85706) 대응을 패치·롤링 업그레이드·포렌식·프록시 차단까지 런북으로 정리합니다.

GitLab CVE-2026-85706 야생 악용: 외부 노출 인스턴스 운영 런북

타임라인과 운영 관점의 의미

  • 2026-09-10: GitLab이 self-managed GitLab CE/EE에 대해 긴급 패치 릴리스(19.3.2/19.2.6/19.1.8)를 공개했습니다. 패치 노트에는 repository commits API의 Path Traversal로 unauthenticated 임의 파일 읽기가 가능했다고 명시되어 있습니다1.
  • 2026-09-11: Rapid7에 따르면 CVE-2026-85706이 CISA KEV에 추가되었고(활성 악용 증거 기반), 연방 기관 기준 remediation due date가 2026-09-14로 설정되었으며 BOD 26-04의 forensic triage 요구사항 대상이라고 정리되어 있습니다2.
  • 2026-09-16: CERT-In이 CIVN-2026-0457에서 CVE-2026-85706이 야생에서 악용 중이라고 적시했습니다3.
  • 2026-09-17: Singapore CSA도 Active Exploitation을 경고하며 즉시 패치할 것을 권고했습니다4.

운영 관점에서 이 타임라인이 의미하는 바는 단순합니다. 패치 릴리스가 나왔다는 사실보다, “노출된 인스턴스는 이미 스캔/프로빙의 대상이 됐고, 패치만으로 사건이 끝나지 않는다”가 핵심입니다. GitLab이 제공한 탐지 시그니처(정확히는 TLDR 저장소의 Sigma 룰 3종)도 이 흐름을 반영합니다1.

아래 런북은 외부 노출 GitLab을 기준으로, (1) 어디가 뚫리는지의 구체적인 접점, (2) 긴급 패치/롤링 업그레이드 순서, (3) 로그 기반 포렌식과 흔적 점검, (4) 패치 적용 전후의 차단(Reverse Proxy/WAF)을 한 덩어리로 묶은 운영 절차입니다.

실제로 어디가 뚫리는가: Commits API, multipart 파라미터, 그리고 우회

이번 이슈를 “Path Traversal 하나”로 뭉뚱그리면 대응이 빈약해집니다. 운영자가 확인해야 할 것은 기능 이름이 아니라 외부에서 때릴 수 있는 HTTP 접점과 파라미터 모양입니다.

1) 1차 접점: POST /api/v4/projects/:id/repository/commits

GitLab 지원 문서(서포트 아티클) 기준으로 CVE-2026-85706은 unauthenticated path traversal이며, 공격자가 POST /api/v4/projects/:id/repository/commits 요청에서 file.path 파라미터로 서버 측 임의 경로를 공급할 수 있다고 설명합니다. 핵심은 서버가 “업로드 버퍼 파일”을 열어야 하는데, 그 대신 공격자가 준 경로를 연다는 점입니다5.

중요한 운영 포인트는 두 가지입니다.

  • multipart가 필수 조건이 아닙니다. GitLab은 application/x-www-form-urlencoded 바디로도 도달 가능하다고 명시합니다5.
  • 단순히 “요청이 200이면 유출” 같은 규칙으로 판정하면 틀릴 수 있습니다. 아래에서 설명하듯, 이 취약점은 파일을 읽는 동작과 클라이언트로 바이트가 나가는 동작이 분리되어 있습니다5.

2) 변종/동일 클래스 접점: metadata.path 및 Repository Files API

GitLab Security의 TLDR 룰은 file.path뿐 아니라 metadata.path도 같은 클래스의 LFI 시그널로 취급합니다6.

또한 동일 룰 문서에서 “primary vector는 commits API”라고 적으면서도, repository files API도 영향을 받는다고 명시합니다76.

즉, 운영자가 방화벽/프록시에서 임시 차단을 고민할 때 repository/commits만 보고 끝내면 부족할 가능성이 있습니다. 다만 “무조건 다 막자”는 접근도 실제 서비스 영향이 큽니다. 이 균형은 뒤의 차단 섹션에서 다룹니다.

3) “under certain conditions”의 실체를 운영자가 확인해야 하는 이유

GitLab 패치 노트는 반복해서 under certain conditions를 붙입니다1. 외부 분석에서는 “public-facing 인스턴스, public project 존재” 같은 전제조건을 언급하기도 하지만, 이 글에서는 GitLab이 공식적으로 단정하지 않은 조건을 사실로 단정하지 않습니다.

대신 운영자가 확인할 수 있는 조건을 실무적으로 정리하면 다음 두 가지입니다.

  • 인터넷에서 /api/v4/projects/:id/repository/commits로 요청이 도달하는가(Reverse Proxy/Ingress 포함)
  • API 로그(api_json.log)에 unauthenticated client(meta.client_idip/<addr> 형태)로부터 file.path / metadata.path가 관측되는가(아래 포렌식 섹션에서 구체화)

이 두 가지가 만족되면 “조건을 해석하는 회의”보다 “패치 + 흔적 점검”으로 바로 넘어가는 편이 빠릅니다.

유출 판정의 함정: 파일은 읽혔을 수 있고, 바이트는 안 나갔을 수 있다

이번 건을 더 까다롭게 만드는 부분은 “임의 파일 읽기”라는 표현이 곧바로 “임의 파일 내용 다운로드”를 뜻하지 않을 수 있다는 점입니다.

GitLab 지원 문서는 다음을 명확히 구분합니다.

  • 서버는 매번 파일을 읽습니다.
  • 하지만 클라이언트로 콘텐츠가 전달되는 것은 파일 내용의 percent-decoding이 실패해서, 파싱 에러 메시지에 문제 구간 조각이 포함될 때라는 조건을 둡니다.
  • 따라서 HTTP status code만으로는 유출 여부를 결론낼 수 없고, written_bytes(Workhorse access log) 및 api_error(Rails api_json.log)를 이용해 실제로 전송된 바이트를 확인해야 한다고 설명합니다5.

이건 운영 런북에 그대로 반영되어야 합니다.

  • “패치 전 기간에 commits API로 이상 요청이 있었다”는 사실은 침해 가능성을 올립니다.
  • “유출 바이트가 실제로 발생했다”는 사실은 즉시 비밀정보 회전(특히 외부 연동 자격증명, DB 크리덴셜, GitLab secret material)로 이어져야 합니다.

또 하나 중요한 함정이 있습니다. GitLab 지원 문서는 “유출이 없었다”가 “파일이 읽히지 않았다”를 의미하지 않는다고 못 박습니다5. 이 때문에 사건 종결 기준을 “다운로드 흔적이 없으니 안전”으로 잡으면 위험합니다.

긴급 조치: 패치 전에 해야 하는 최소한의 격리와 보존

패치가 최우선이지만, 패치 과정에서 재시작/로그 로테이션/컨테이너 재배치가 발생해 증거가 날아가는 경우가 많습니다. GitLab 지원 문서도 “업그레이드/재시작 전에 Workhorse 로그가 로테이트될 수 있으니 먼저 증거 보존”을 권고합니다5.

1) 외부 노출 차단(완화) 우선순위

아래는 패치까지의 시간을 벌기 위한 임시 완화이며, “안전한 대체”가 아닙니다. watchTowr도 public-facing self-hosted는 patch 또는 public access 제거를 직접적으로 언급합니다8.

  • 최선: 외부에서 GitLab 자체를 접근 불가로 전환(사내 VPN/제로트러스트 경유)
  • 차선: Reverse Proxy/WAF에서 POST /api/v4/projects/*/repository/commits 경로를 외부에서 차단
  • 그 다음: file.path/metadata.path 파라미터 문자열 기반 룰을 추가(단, 인코딩/변형/우회에 취약)

임시 차단은 “exploit 시도”를 줄이는 데는 도움이 되지만, 이미 시도가 있었다면 흔적 점검과 자격증명 회전의 필요성을 없애지 않습니다.

2) 증거 보존 체크리스트(Omnibus 기준)

GitLab 지원 문서가 제시하는 보존 대상은 다음입니다5.

  • /var/log/gitlab/gitlab-rails/api_json.log*
  • /var/log/gitlab/gitlab-workhorse/current
  • /var/log/gitlab/nginx/ (Reverse Proxy가 별도라면 해당 프록시 액세스 로그 포함)

Helm/Kubernetes는 Workhorse가 sidecar일 수 있으니 해당 컨테이너 로그를 포함해야 한다고도 명시합니다5.

패치/롤링 업그레이드: “버전만 올리면 끝”이 아닌 이유

1) 영향 범위와 고정 버전

GitLab 공식 패치 노트 기준 영향 범위는 다음과 같습니다.

  • 영향: GitLab CE/EE 18.7 이상에서 19.1.8 미만, 19.2.6 미만, 19.3.2 미만
  • 고정: 19.1.8 / 19.2.6 / 19.3.2

124

Rapid7은 self-managed 배포 형태(Omnibus/source/Helm)가 모두 영향이라고 정리합니다2. GitLab 패치 노트도 특정 배포 타입을 따로 언급하지 않으면 all types affected라는 원칙을 적어둡니다1.

2) 다운타임/마이그레이션 포인트

Rapid7은 “업데이트에 DB migrations가 포함”되며 single-node는 migrations 동안 다운타임이 발생할 수 있고, multi-node는 GitLab의 zero-downtime upgrade 절차를 사용할 수 있다고 적습니다. 또한 고정 릴리스 중 19.3.2만 post-deployment migrations가 포함된다고 명시합니다2.

운영 런북에서 중요한 건 여기입니다.

  • 무조건 “지금 당장”을 외쳐도, DB migration에서 멈추면 더 큰 장애로 번집니다.
  • 그렇다고 change window를 기다리면 그 시간 동안 외부 노출 면이 유지됩니다.

나는 이런 류의 긴급 패치에서는 “롤링 업그레이드 설계”보다 “업그레이드 중단 시 되돌리기”가 더 중요하다고 봅니다. 즉, 업그레이드 절차의 1/3은 실행이고 2/3는 실패 대비입니다.

3) 긴급 업그레이드 체크리스트(실행 전)

  • 현재 버전/설치 형태/단일 vs 다중 노드 확인
  • 백업(특히 DB + secrets material)과 롤백 경로 확인
  • 업그레이드 전 로그 보존 완료 여부 확인(앞 섹션)
  • 외부 노출 임시 차단 적용(가능하다면)
  • 업그레이드 후 검증(버전 확인 + API 접근 로그에서 더 이상 시도/성공이 없는지)

버전 확인 자체는 환경별로 다르지만, GitLab 패치 노트가 “Find your GitLab version” 링크를 제공하는 것처럼, 조직 내 표준 운영 명령을 써서 설치 버전과 배포 타입을 확정해야 합니다1.

포렌식/헌팅: api_json.log → correlation_id → Workhorse written_bytes

GitLab이 이번 건에서 특이하게 좋은 점은 “탐지 룰 3종 + 로컬 grep 쿼리”를 공식 오픈소스 저장소 형태로 공개했다는 부분입니다. 패치 노트가 직접 이 룰들을 링크합니다1.

또한 GitLab 문서는 api_json.log / production_json.log 등 로그 위치와 성격을 명확히 설명합니다. 특히 API 요청은 별도의 api_json.log로 분리된다는 점이 이번 헌팅에 유리합니다9.

1) 1차 필터: api_json.log에서 unauthenticated + file.path/metadata.path

GitLab 지원 문서는 api_json.log에서 file.path/metadata.path를 추출하고 correlation_id를 함께 기록하는 파이프라인을 제공합니다5.

추출 시 주의점도 명시합니다.

  • route literal에만 의존하지 말 것(인코딩 우회 가능)
  • 파라미터 키도 percent-encoding될 수 있으니 file%2epath 같은 변형을 함께 볼 것

이 지점이 “Reverse Proxy/WAF 차단 룰을 대충 문자열 매칭으로 떼우면 안 된다”는 근거이기도 합니다.

2) 2차 판정: Workhorse의 written_bytesapi_error

GitLab 지원 문서가 제시하는 방식은 다음 흐름입니다.

1) api_json.log에서 correlation_id 후보 리스트를 만든다. 2) Workhorse access log에서 correlation_id를 찾아 written_bytes를 확인한다. 3) api_error가 존재하는지 확인한다. 4) not-found 기준선(문서 예시에서는 54 bytes body)을 잡고 비교한다.

이 방식의 장점은 “데이터 유출 여부”를 로그 기반으로 어느 정도 정량화할 수 있다는 점입니다5.

3) GitLab Security가 제공하는 3종 탐지 룰을 그대로 가져오기

패치 노트가 링크한 TLDR 룰은 세 가지 방향을 잡고 있습니다.

  • 특정 고가치 파일(gitlab.yml, secrets.yml, database.yml, gitlab-secrets.json) 타깃을 high-signal로 탐지7
  • metadata.path 변종 탐지6
  • 업로드 sandbox(/uploads/tmp/) 밖의 file.path를 광범위하게 보고, 동일 IP의 다수 경로 시도를 burst로 보는 enumeration 탐지10

운영자 입장에서는 3개 모두 “탐지”라는 목적은 같지만 의미는 다릅니다.

  • 고가치 파일 타깃 탐지는 한 건만 떠도 escalation 근거가 충분합니다.
  • path enumeration burst는 “누가 스캔했는지”와 “언제부터 맞았는지”를 잡는 데 좋습니다.

실제로 실행 가능한 헌팅 스크립트(Omnibus 로그 기준)

아래 스크립트는 GitLab 지원 문서의 Step 2/3/5 흐름을 Omnibus 단일 노드 기준으로 묶은 것입니다. SIEM이 없고, 우선 오늘(2026-09-20) 즉시 판단이 필요할 때 빠르게 돌릴 목적입니다.

전제:

  • jq가 설치돼 있어야 합니다.
  • 로그가 로테이션되어 api_json.log.1, api_json.log.2.gz 형태로 존재할 수 있으니, 필요하면 zgrep/병합을 별도로 해야 합니다.
  • Workhorse 로그는 /var/log/gitlab/gitlab-workhorse/current를 가정합니다5.
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env bash
set -euo pipefail

RAILS_API_JSON=${RAILS_API_JSON:-/var/log/gitlab/gitlab-rails/api_json.log}
WORKHORSE=${WORKHORSE:-/var/log/gitlab/gitlab-workhorse/current}
OUTDIR=${OUTDIR:-./cve-2026-85706-hunt-$(date +%Y%m%d%H%M%S)}

mkdir -p "$OUTDIR"

# 1) 후보 추출: api_json.log에서 file.path / metadata.path 등장 라인
# GitLab 지원 문서 Step 2를 기반으로 correlation_id, status, key, value, remote_ip를 뽑습니다.
# percent-encoded key(file%2epath 등)도 '.'로 정규화합니다.

echo "[+] Extract candidates from $RAILS_API_JSON"

grep -F '"params"' "$RAILS_API_JSON" \
  | grep -iE '"(file|metadata)(\.|%2E)path"' \
  | jq -r '
      . as $e
      | ($e.params // [])[]
      | select((.key | ascii_downcase | gsub("%2e"; ".")) == "file.path"
            or (.key | ascii_downcase | gsub("%2e"; ".")) == "metadata.path")
      | [$e.correlation_id,
         ($e.status|tostring),
         (.key|tostring),
         (.value|tostring),
         ($e["meta.remote_ip"] // $e.remote_ip // "")]
      | @tsv
    ' > "$OUTDIR/candidates.tsv"

# 2) Workhorse written_bytes 매핑
# GitLab 지원 문서 Step 3을 기반으로 correlation_id로 access 로그를 찾아 written_bytes 추출

echo "[+] Map written_bytes from $WORKHORSE"

echo -e "correlation_id\tstatus\tparam_key\tparam_value\tremote_ip\twritten_bytes\tworkhorse_status\turi\tduration_ms" \
  > "$OUTDIR/candidates_with_bytes.tsv"

while IFS=$'\t' read -r corr_id status key value rip; do
  # Workhorse 로그는 JSON 라인이 섞여 있으니 correlation_id 포함 라인 중 access msg만 필터
  wb_json=$(grep -F "\"$corr_id\"" "$WORKHORSE" \
    | grep -F '"msg":"access"' \
    | tail -n 1 \
    | jq -c '{written_bytes, status, uri, duration_ms}' 2>/dev/null || true)

  if [[ -z "${wb_json}" ]]; then
    echo -e "${corr_id}\t${status}\t${key}\t${value}\t${rip}\t\t\t\t" \
      >> "$OUTDIR/candidates_with_bytes.tsv"
    continue
  fi

  written_bytes=$(jq -r '.written_bytes // empty' <<<"$wb_json")
  wh_status=$(jq -r '.status // empty' <<<"$wb_json")
  uri=$(jq -r '.uri // empty' <<<"$wb_json")
  dur=$(jq -r '.duration_ms // empty' <<<"$wb_json")

  echo -e "${corr_id}\t${status}\t${key}\t${value}\t${rip}\t${written_bytes}\t${wh_status}\t${uri}\t${dur}" \
    >> "$OUTDIR/candidates_with_bytes.tsv"

done < "$OUTDIR/candidates.tsv"

# 3) api_error 조각 추출(있는 경우)
# GitLab 지원 문서 Step 5/추출 예시를 참고하여 api_error[0]를 복구합니다.

echo "[+] Extract api_error fragments (if any)"

cut -f1 "$OUTDIR/candidates.tsv" | sort -u | while read -r corr_id; do
  # api_error가 없는 경우 empty로 끝납니다.
  grep -F "\"$corr_id\"" "$RAILS_API_JSON" \
    | jq -r '.api_error[0]? // empty' \
    | jq -r '.message
        | ltrimstr("400 Bad request - Invalid parameter: invalid %-encoding (")
        | .[:-1]' \
    > "$OUTDIR/api_error_fragment_${corr_id}.txt" || true

  # 빈 파일이면 삭제
  if [[ ! -s "$OUTDIR/api_error_fragment_${corr_id}.txt" ]]; then
    rm -f "$OUTDIR/api_error_fragment_${corr_id}.txt"
  fi

done

echo "[+] Done. Outputs:"
ls -la "$OUTDIR"

예상 산출물:

  • candidates.tsv: file.path/metadata.path가 관측된 요청 목록(기본 스코프)
  • candidates_with_bytes.tsv: 위 목록에 대해 Workhorse written_bytes가 매핑된 결과
  • api_error_fragment_*.txt: 실제 유출 조각이 로그에 남은 경우에만 생성

여기서 내가 중요하게 보는 건 두 가지입니다.

1) api_error_fragment_*.txt가 존재한다면, 그 내용은 “공격자가 받은 바이트 조각”이므로 단순 IOC가 아니라 실제 비밀정보 회전의 우선순위 근거가 됩니다5. 2) fragment가 없다 해도 파일 read 시도는 있었을 수 있고, 그 자체가 “GitLab 호스트에 저장된 비밀정보가 공격자에게 노출될 뻔했다”는 운영 리스크로 남습니다5.

Reverse Proxy/WAF 임시 차단: 무엇을 막고 무엇을 포기하는가

패치 적용 전 몇 시간이라도 버티기 위해 프록시에서 막는 시나리오가 흔합니다. watchTowr는 commits API로 향하는 POST + file.path 파라미터를 로그에서 헌팅하라고 구체적으로 적고 있고8, GitLab도 file.path/metadata.path를 중심으로 탐지 룰을 제공합니다10.

하지만 “프록시에서 완벽 차단”은 보통 불가능합니다. 이유는 간단합니다.

  • 인코딩/변형(예: 키의 percent-encoding)까지 모두 커버하는 룰은 오탐/누락 양쪽으로 불안정해집니다.
  • NGINX 같은 L7 프록시가 request body 파라미터까지 안정적으로 검사하기는 어렵습니다.

그래서 런북에서는 차단을 두 레벨로 나눕니다.

레벨 A: 경로 기반 차단(가장 단순, 영향은 명확)

외부에서 GitLab의 commit-create API를 쓸 일이 거의 없다면, 아래처럼 repository/commits POST만 외부에서 막는 전략이 가장 단순합니다.

1
2
3
4
5
6
# 예시: 인터넷 구간에서만 차단하는 별도 server 블록/ingress 적용을 전제로 합니다.
location ~ ^/api/v4/projects/[^/]+/repository/commits/?$ {
    if ($request_method = POST) {
        return 403;
    }
}

이 방식은 우회 인코딩이나 파라미터 변형을 신경 쓰지 않아도 됩니다. 대신 기능 영향이 있습니다.

  • Web UI나 자동화가 commits API로 파일 생성/수정을 하는 경우(특히 외부 자동화)가 깨질 수 있습니다.

나는 외부 노출 인스턴스라면 기능 영향이 있더라도 “외부에서 commits API POST를 잠시 막고, 내부망/VPN에서만 허용” 쪽이 운영적으로 예측 가능하다고 봅니다.

레벨 B: 파라미터 기반 WAF 룰(정밀하지만 유지보수가 필요)

ModSecurity/상용 WAF를 쓰고 request body inspection이 가능하다면, 최소한 아래 문자열을 차단 키워드로 잡습니다.

  • file.path, metadata.path
  • file%2epath, metadata%2epath (GitLab 룰에서도 명시적으로 다룹니다10.)

다만 이 레벨은 “패치 지연을 정당화하는 장치”가 되기 쉽습니다. GitLab과 Rapid7 모두 vendor-supplied update를 즉시 적용하라고 반복하고12, CSA/CERT-In도 같은 톤입니다43.

패치 후에 남는 일: 비밀정보 회전과 사건 종결 기준

이번 건에서 자주 나오는 반론은 “임의 파일 읽기면 정보 유출이지, RCE는 아니지 않나”입니다. 그런데 GitLab Security의 탐지 룰 문서 자체가 이 반론을 반박합니다.

  • gitlab.yml에는 환경에 따라 object storage, SMTP, LDAP 자격증명 등이 들어갈 수 있다고 적습니다.
  • secrets.yml/gitlab-secrets.json은 secret key base 및 세션/토큰/암호화 키들과 엮인다고 명시합니다.
  • database.yml은 DB 크리덴셜이라고 적습니다.

7

즉, “파일 읽기”는 결과적으로 다른 시스템(메일, 디렉터리, 오브젝트 스토리지, 데이터베이스, CI/CD)의 키가 한 번에 풀리는 사고가 될 수 있습니다.

다만 회전 전략은 “유출이 확인됐는지”에 따라 달라져야 합니다.

1) 유출 조각이 확인된 경우(api_error fragment 존재)

GitLab 지원 문서가 말하는 방식대로라면, api_error에 남은 fragment는 공격자가 받은 바이트와 동일합니다5.

이 경우 사건 종결의 기준은 “패치 완료”가 아니라 아래가 됩니다.

  • fragment에 등장하는 credential/token/secret이 어디 시스템의 것인지 식별
  • 해당 시스템에서 키/비밀번호/토큰 회전
  • GitLab에서 그 키를 참조하던 설정/CI 변수/통합 설정 갱신
  • 동일 기간(9/10~패치 시점) 동안 실행된 CI job / Runner 등록 / 배포 이벤트의 이상 징후 점검

여기서 GitLab 자체 비밀정보(gitlab-secrets.json 등) 회전은 영향이 크고, 절차를 틀리면 복구가 더 커집니다. 이 글에서는 GitLab이 문서로 제공하는 공식 절차를 그대로 따르는 것 이상을 임의로 적지 않습니다. 대신 “무엇이 노출되면 무엇이 위험해지는지”를 기반으로 우선순위를 잡는 게 현실적입니다.

2) 유출 조각이 확인되지 않은 경우(api_error fragment 없음)

이 케이스는 애매합니다. GitLab 지원 문서 기준으로, 파일에 invalid percent sequence가 없으면 디코딩이 성공해 에러가 나지 않고, 그 경우 클라이언트로 유출 바이트가 없을 수 있습니다5.

하지만 이건 “안전”이 아니라 “유출 증거가 로그에 남지 않는 형태일 수 있음”에 가깝습니다. 최소한 아래는 해야 합니다.

  • 공격 시도 원격 IP/ASN/UA를 식별하고 차단(단, 이미 스캐너/봇일 수 있음)
  • 같은 원격 IP가 다른 엔드포인트(로그인, OAuth, GraphQL, 패키지 레지스트리, Runner 등록 등)도 두드렸는지 확인
  • path enumeration burst가 있었다면, 그 이후에 성공(2xx/비정상 응답) 패턴이 있는지 확인10

함께 패치된 GraphQL 이슈를 어떻게 볼 것인가

GitLab의 2026-09-10 패치 릴리스는 CVE-2026-85706만 고친 게 아닙니다. 같은 릴리스 노트에 GraphQL subscription serializer의 insecure deserialization(CVE-2026-87719, GitLab EE)이 critical로 묶여 있고, “Duo Chat access가 있는 authenticated user” 조건을 명시합니다1.

운영 우선순위를 나누면 이렇게 됩니다.

  • CVE-2026-85706: unauthenticated + 인터넷 노출이면 즉시 악용 표면
  • CVE-2026-87719: 인증 후(특정 기능 접근) 조건이 붙으므로, 외부 노출 환경에서도 접근 경로가 더 길 수 있음

다만 한 번의 긴급 업그레이드로 둘 다 해결된다면, “이번에 commits API만 막고 GraphQL은 나중에” 같은 분할 대응은 의미가 없습니다. GitLab이 패치 릴리스를 critical로 묶어낸 이유는 결국 “노출면 전체를 빨리 올려라” 쪽에 가깝습니다.

반론과 회의론: 왜 아직도 런북이 필요한가

“패치 버전으로 올리면 끝 아닌가”

Rapid7은 ‘Given the confirmed exploitation’ 이후 업데이트 적용 후에도 compromise 징후를 찾으라고 권고합니다2. GitLab 역시 탐지 룰 3종을 별도로 공개해 self-managed 고객이 흔적 점검을 하도록 유도합니다1.

즉, 이 건은 패치가 종료조건이 아니라 시작조건에 가깝습니다.

“어차피 percent-decoding 조건 때문에 유출이 제한적이지 않나”

GitLab 지원 문서의 설명대로라면, 실제 바이트 유출은 조건이 붙습니다. 하지만 운영 리스크는 여전히 큽니다.

  • 읽기 자체는 매번 발생할 수 있습니다5.
  • config/secrets 파일은 환경마다 내용이 다르고, %를 포함할 가능성도 배제할 수 없습니다.
  • GitLab 호스트는 “개발/배포의 비밀정보 허브” 역할을 하기 때문에, 일부 조각 유출로도 연쇄 사고가 가능합니다7.

나는 이 경우 “유출이 제한적일 수 있으니 안심”이 아니라, “유출이 불완전하고 파편적일 수 있으니 어떤 키가 노출됐는지 더 빨리 식별해야 한다”로 해석하는 편입니다.

앞으로 지켜볼 것: 운영자가 확인해야 할 변화 포인트

  • commits API 차단을 우회하는 새로운 요청 변형이 나오는지(인코딩/라우팅 변형). GitLab 지원 문서가 route 인코딩 우회를 염두에 두고 필터링을 권고한 것 자체가 신호입니다5.
  • 로그 수집 파이프라인이 api_error를 잘라먹고 있는지. GitLab 지원 문서는 log shipper가 truncation을 일으키면 판정이 어려워진다고 경고합니다5.
  • 조직의 패치 우선순위 체계에서 KEV 반영이 자동화되어 있는지. 예전에 정리해둔 CISA KEV로 패치 우선순위 엔진 만들기의 흐름대로라면, 이번 케이스는 “긴급 패치 + triage”가 같이 발동되는 유형입니다.

결론적으로 CVE-2026-85706은 단순한 패치 공지가 아니라, 외부 노출 GitLab을 “즉시 업그레이드해야 하는 시스템”에서 “이미 맞았을 수 있는 시스템”으로 취급하게 만드는 사건입니다.

참고 자료

  1. https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-3-2-released/ ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8 ↩︎9 ↩︎10

  2. https://www.rapid7.com/blog/post/etr-cve-2026-85706-critical-gitlab-path-traversal-exploited-in-the-wild/ ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6

  3. https://www.cert-in.org.in/s2cMainServlet?VLCODE=CIVN-2026-0457&pageid=PUBVLNOTES01 ↩︎ ↩︎2

  4. https://www.csa.gov.sg/alerts-and-advisories/alerts/al-2026-123/ ↩︎ ↩︎2 ↩︎3

  5. https://support.gitlab.com/hc/en-us/articles/30371082139164-Validating-whether-a-file-was-actually-exfiltrated-via-CVE-2026-85706-using-Workhorse-written-bytes ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8 ↩︎9 ↩︎10 ↩︎11 ↩︎12 ↩︎13 ↩︎14 ↩︎15 ↩︎16 ↩︎17 ↩︎18

  6. https://gitlab.com/gitlab-security-oss/tldr/-/raw/main/Detections/platforms/GitLab/Vulnerability%20Detections/local_file_inclusion_metadata_path.md ↩︎ ↩︎2 ↩︎3

  7. https://gitlab.com/gitlab-security-oss/tldr/-/raw/main/Detections/platforms/GitLab/Vulnerability%20Detections/local_file_inclusion_gitlab_yml_read.md ↩︎ ↩︎2 ↩︎3 ↩︎4

  8. https://watchtowr.com/intelligence/rapid-reaction-gitlab-critical-path-traversal-vulnerability-cve-2026-85706/ ↩︎ ↩︎2

  9. https://docs.gitlab.com/administration/logs/ ↩︎

  10. https://gitlab.com/gitlab-security-oss/tldr/-/raw/main/Detections/platforms/GitLab/Vulnerability%20Detections/local_file_inclusion_path_enumeration.md ↩︎ ↩︎2 ↩︎3 ↩︎4

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.