CVE-2026-110264 (GitLab Org-mode 정수 오버플로 RCE) 분석
CVE-2026-110264 (GitLab Org-mode 정수 오버플로 RCE) 분석
CVE · 웹·서버 · RCE · CVE-2026-110264 · [심층]
덱. 공개 저장소에 올린
.org문서 파일 하나로, 로그인 없이 GitLab 서버에서 코드를 실행할 수 있었습니다. 원인은 멀리 있지 않았습니다 — 사용자가 적어 넣은 한 “단어”가 그대로 정규식(regular expression)이 되어, 정규식 엔진 안의 16비트짜리 작은 칸을 넘겨 버린 것입니다. 이 글은 공개된org-ruby소스와 GitLab이 직접 낸 탐지 문서를 열어, 이 CVSS 10.0 취약점의 근본원인·공격 흐름·영향을 우리 관점에서 분석합니다.
사용자가 넣은 값이 “데이터”로만 쓰이면 안전합니다. 문제는 그 값이 어느 순간 “코드”가 될 때입니다. SQL 인젝션은 사용자 문자열이 질의가 되고, 템플릿 인젝션은 사용자 문자열이 템플릿이 됩니다. 이 글의 버그는 같은 부류의 교과서적 사례입니다 — 사용자가 쓴 글자가 정규식(코드)이 되어, 그 정규식을 컴파일하던 엔진의 내부 카운터를 넘겨 메모리를 망가뜨렸습니다. 한 틈은 고쳐졌지만, “입력을 정규식으로 만든다”는 설계는 GitLab 한 곳에서만 최근 한 달에 세 번 터졌습니다.
개요
GitLab은 2026년 10월 22일, 자체 관리형(self-managed) 인스턴스를 노리는 긴급(Critical) 패치를 냈습니다. 주력 취약점 CVE-2026-110264는 CVSS 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)으로, 인증 없이(PR:N) 원격에서 서버 임의 코드 실행(RCE)에 이릅니다. 영향 범위(Scope)가 바뀜(S:C)으로 잡힌 건, 취약한 컴포넌트를 넘어 GitLab 서버 프로세스 전체(git 계정 권한)가 장악된다는 뜻입니다. [확인]
공식 CWE 분류는 **CWE-190 (정수 오버플로/랩어라운드)**입니다. 다만 이 한 줄은 “산술이 어디서 넘쳤나”만 담습니다. 실제 피해 사슬은 세 가지가 맞물립니다 [확인]:
- CWE-94 — 사용자 입력이 정규식 소스로 재주입(코드 생성 제어 미흡)
- CWE-190 — 반복 횟수가 16비트 필드를 넘겨 랩어라운드
- CWE-787 — 그 결과로 실행(매치) 단계에서 경계 밖 쓰기(Out-of-Bounds Write)
제보는 HackerOne 버그바운티를 통해 연구자 joaxcar가 했습니다. [확인]
영향받는 버전
| 제품 | 취약 버전 | 안전(패치) 버전 |
|---|---|---|
| GitLab CE/EE | 7.0 이상 ~ 18.11.13 미만 | 18.11.13 |
| GitLab CE/EE | 19.0 ~ 19.0.10 미만 | 19.0.10 |
| GitLab CE/EE | 19.1 ~ 19.1.9 미만 | 19.1.9 |
| GitLab CE/EE | 19.2 ~ 19.2.9 미만 | 19.2.9 |
| GitLab CE/EE | 19.3 ~ 19.3.6 미만 | 19.3.6 |
| GitLab CE/EE | 19.4 ~ 19.4.3 미만 | 19.4.3 |
| GitLab CE/EE | 19.5 ~ 19.5.1 미만 | 19.5.1 |
GitLab.com과 GitLab Dedicated는 이미 패치됐습니다. 조치가 필요한 건 자체 관리형 인스턴스입니다. 18.11.13 미만 백포트 라인까지 패치가 제공된 점으로 보아, 아주 오래된 7.x~18.10 사용자는 지원 라인(최소 18.11.13)으로 올리라는 뜻으로 읽힙니다. [분석]
기술적 배경
Org-mode는 Emacs에서 온 문서 형식(.org)으로, 할 일 목록·메모·문서를 평문으로 적는 마크업입니다. GitLab은 저장소의 .org 파일을 웹에서 볼 때 이를 HTML로 바꿔 보여주는데, 이때 **org-ruby**라는 서드파티 젬(gem, 버전 0.9.12)을 씁니다. [확인]
Org-mode에는 TODO 키워드를 사용자가 직접 정의하는 기능이 있습니다. 문서 맨 위에 #+TODO: 줄을 적으면, 그 단어들이 할 일 상태(예: TODO, DONE, 사용자가 만든 임의 단어)로 쓰입니다. org-ruby는 헤드라인(제목 줄)을 파싱할 때, 첫 단어가 이 키워드인지 확인하고 맞으면 제거합니다.
여기서 핵심은 정규식 엔진입니다. Ruby는 Onigmo라는 정규식 엔진을 코어에 기본 내장합니다(Oniguruma에서 갈라져 나와 CRuby 2.0부터 번들된 표준 컴포넌트입니다 — GitLab이 따로 포크해 만든 게 아닙니다 [확인]). Onigmo는 정규식을 컴파일할 때, “이게 몇 번째 반복 구문인가”를 가리키는 repeat ID를 내부에 저장합니다. 그런데 이 ID를 담는 칸이 부호 있는 16비트(signed 16-bit) — 즉 최대 32,767까지만 담깁니다. 이 작은 칸이 이 사건의 무대입니다.
취약점 발생 원인 [패스 A — 구조/정적: 미흡점 열거]
이 취약점은 하나의 큰 실수가 아니라, 네 겹의 작은 틈이 순서대로 포개진 결과입니다.
A-1. 사용자 키워드를 escape 없이 정규식 소스로 재주입 (CWE-94)
org-ruby는 TODO 키워드 목록을 만들 때는 각 단어를 정규식 메타문자로부터 escape 합니다. 그런데 그 다음, 매치된 키워드를 제거하려고 두 번째 정규식을 새로 만들 때는 그 단어를 날것 그대로 집어넣습니다.
# 개념용 단순화 — 실제 페이로드/원본 코드 아님
def parse_keywords
# ... 첫 단어가 키워드 정규식에 매치되면 @keyword 에 담긴다 ...
@keyword = words[0]
# ↓ @keyword(사용자 문자열)가 escape 없이 새 정규식의 소스가 된다
@headline_text.sub!(Regexp.new("^#{@keyword}\\s*"), "")
end
실제 콜사이트 요약 인용 — org-ruby lib/org-ruby/headline.rb의 핵심 한 줄을 요약한 것입니다(완성형 페이로드 아님).
- 어디:
org-rubyheadline.rb의 키워드 파싱 지점. - 없는 검증:
@keyword를 정규식 소스로 쓰기 전에 escape 하거나, 길이·복잡도를 제한하지 않음. - 그래서 가능해지는 것: 공격자가
.org문서의#+TODO:줄에 매치되도록 조작한 “키워드”를 넣으면, 그 문자열이 통째로 정규식 패턴이 됩니다 — 즉 공격자가 컴파일될 정규식의 모양을 직접 설계할 수 있습니다.
A-2. 반복 횟수 상한 검증 부재 → 16비트 repeat ID 랩어라운드 (CWE-190)
A-1 덕분에 공격자가 정규식에 반복 구문을 아주 많이 넣을 수 있게 되자, Onigmo의 16비트 칸이 문제가 됩니다. 반복 구문이 32,767개를 넘으면 repeat ID가 16비트 한계를 넘어 음수나 작은 값으로 순환(wrap) 합니다. [확인]
- 어디: Onigmo 컴파일러가 repeat ID를 할당하는 지점.
- 없는 검증: “repeat 개수가 필드 상한을 넘는가”를 컴파일 전에 막는 검사.
- 그래서 가능해지는 것: 컴파일은 성공한 것처럼 끝나지만, 내부 바이트코드에는 잘못된(음수/범위 밖) ID가 박힙니다.
A-3. 렌더링 입력에 크기·복잡도 가드 부재
GitLab은 마크업 렌더링에 **시간 예산(timeout)**만 걸어 둡니다. “너무 오래 걸리면 중단”은 있어도, “이 키워드가 비정상적으로 길거나 반복이 많다”는 입력 자체의 이상을 걸러내는 가드는 없었습니다. [분석]
- 그래서 가능해지는 것: 악성
.org가 렌더 파이프라인 끝까지 �러들어가 Onigmo에 도달합니다.
A-4. 랩어라운드된 ID가 매치 단계에서 포인터 연산에 사용 (CWE-787)
여기서 advisory의 표현을 정확히 봐야 합니다. 공식 권고는 “정규식을 컴파일할 때(compiling) 정수 오버플로”라고 적었는데, 이는 산술이 넘치는 지점(컴파일)을 가리킬 뿐입니다. 실제 메모리 손상은 매치(match) 단계에서 터집니다 [확인] — 잘못된 repeat ID로 만들어진 옵코드가 실행되며 내부 배열을 음수/범위 밖으로 인덱싱해 버퍼 바깥에 씁니다. GitLab 탐지 문서의 크래시 스택도 컴파일 함수가 아니라 매치 함수(match_at → onig_search_gpos) 쪽을 가리킵니다.
취약점 공격 과정
공격 과정 요약
[공격자] 악성 #+TODO: 줄이 든 .org 파일을 공개 프로젝트에 올림
│
▼
[피해자측 트리거] 누군가(혹은 봇·프리뷰)가 그 .org blob을 "본다"
│ → 인증 불필요. public 프로젝트면 아무나 열람 = 렌더 트리거
▼
[GitLab 마크업 파이프라인] Gitlab::OtherMarkup → org-ruby 로 .org 렌더
│
▼
[org-ruby] 헤드라인 파싱 중 사용자 키워드를 Regexp.new 에 raw 재주입 (A-1)
│
▼
[Onigmo 컴파일] 32,767 초과 반복 → 16비트 repeat ID wrap (A-2) — "성공"한 척 끝남
│
▼
[Onigmo 매치] 잘못된 ID로 버퍼 밖 쓰기(OOB write) (A-4)
│
▼
[격상] 준비된 힙 배치 + 캐리어로 git 계정 코드 실행
상세 공격 과정 [패스 B — 흐름/동적: 호출 순서]
- 공격자가 공개 프로젝트에 조작된
#+TODO:줄을 가진.org파일을 둡니다. 특별한 권한이 필요 없습니다 — 공개 프로젝트를 만들 수만 있으면 됩니다.[확인] - 그 파일이 웹에서 렌더링되는 순간(저장소에서 파일을 열거나 프리뷰가 뜰 때) GitLab 마크업 파이프라인(
Gitlab::OtherMarkup,gitlab-markup젬)이org-ruby를 호출합니다. 보는 사람이 로그인했는지와 무관합니다[확인]— 공개 blob은 누구에게나 렌더됩니다. 렌더를 트리거하는 컨트롤러 경로도 GitLab 탐지 문서의 샘플 로그에 그대로 드러나 있습니다 — 저장소 파일 보기는Projects::BlobController#show, 프리뷰는#preview경로입니다.[확인] org-ruby가 헤드라인을 파싱하며 A-1의Regexp.new("^#{@keyword}…")를 호출합니다. 공격자 문자열이 정규식 소스가 됩니다.[확인]- Onigmo가 이 정규식을 컴파일합니다. 32,767개를 넘는 반복이 16비트 repeat ID를 랩어라운드시킵니다(A-2). 컴파일은 오류 없이 끝난 것처럼 보입니다.
[확인] - 곧바로 매치가 실행됩니다(
sub!가 내부적으로 매치를 돌립니다). 잘못된 ID가 포인터 연산에 쓰여 할당된 버퍼 밖에 씁니다(A-4).[확인] - 공격자가 요청 내용으로 힙 배치를 어느 정도 다듬어 두면, 이 한 번의 경계 밖 쓰기를 함수 포인터·디스패치 테이블 변조로 이어
git계정 권한의 코드 실행까지 격상시킬 수 있습니다 — 다만 이 격상 단계의 구체적 성공 조건은 공개 범위 밖이며, 일반적 메모리 손상 격상 메커니즘에 근거한 설명입니다.[분석]이 글은 격상에 쓰이는 완성형 익스플로잇·페이로드를 싣지 않습니다(랩 공개 윤리).
탐지상 중요한 성질
[확인]: GitLab은 “성공한 익스플로잇은 조용하며, 안정적으로 크래시를 내지 않는다”고 밝혔습니다. 즉 크래시·타임아웃은 실패한 시도의 부산물일 뿐, 탐지는 확정이 아니라 보강 신호로 봐야 합니다.
미흡점 ↔ 공격 흐름 매핑
| 미흡점 (패스 A) | 발화 지점 (패스 B) | 여는 프리미티브 | 다음으로 잇는 것 |
|---|---|---|---|
| A-1 키워드 raw 재주입 | 3단계 Regexp.new 호출 | 공격자 통제 정규식 소스 | 반복 개수를 공격자가 설계 |
| A-2 16비트 ID 랩어라운드 | 4단계 Onigmo 컴파일 | 잘못된 내부 repeat ID | ”성공”으로 위장된 손상 바이트코드 |
| A-3 입력 크기 가드 부재 | 2단계 렌더 진입 | 악성 입력이 엔진까지 도달 | (A-1·A-2가 실제로 실행됨) |
| A-4 ID의 포인터 사용 | 5단계 매치 실행 | 경계 밖 쓰기(OOB write) | 힙 배치→코드 실행 격상 |
이 표가 “빠진 검증의 목록(정적)“과 “한 요청이 그걸 어떻게 꿰는가(동적)“를 잇는 다리입니다.
취약점 패치 + 패치 완전성 평가
GitLab 내부 이슈(work_items/633633)의 실제 코드 diff는 패치 후 90일간 비공개입니다(GitLab 표준 공개 정책). 따라서 “GitLab이 org-ruby 호출부에서 키워드 길이를 제한했는지, 입력을 escape 했는지, 혹은 Onigmo 쪽을 손봤는지”의 구체는 지금 공개 범위 밖입니다 [확인=비공개 사실]. 아래 완전성 평가는 그래서 가설이며, 동적 확증 전까지 단정하지 않습니다.
Q1 — 근본원인을 막았나, 증상만 막았나? 미확인입니다. 패치 구현이 공개되지 않아, 애플리케이션 레벨 필터(증상 완화)인지 근본 설계 수정인지 알 수 없습니다. 아래 Q3의 전례를 보면 이 질문은 특히 조심해야 합니다. [분석]
Q2 — 같은 뿌리의 다른 입구(형제 싱크)는? 있습니다, 그것도 실증된 형태로. 같은 Onigmo 정수 오버플로 계열이 최근 한 달에 세 번 패치됐습니다 [확인]:
| CVE | 공개일 | 입력 경로 | 넘친 필드 | 인증 | CVSS |
|---|---|---|---|---|---|
| CVE-2026-93577 | 2026-09-24 | CI rules:*:regexp | 32비트 점프 오프셋 | 필요 | 9.9 |
| CVE-2026-97229 | 2026-10-13 | CI rules:*:regexp (93577 우회) | 32비트 점프 오프셋 | 불필요 | 10.0 |
| CVE-2026-110264 | 2026-10-22 | Org-mode #+TODO: | 16비트 repeat ID | 불필요 | 10.0 |
세 건 모두 “고정폭 정수 필드 + 사용자 통제 정규식 컴파일”이라는 한 뿌리에서 나왔지만, 넘친 필드와 입력 경로가 각각 다릅니다. 즉 Onigmo 컴파일 경로에는 “사용자가 넘길 수 있는 작은 칸”이 여러 곳 있었고, GitLab은 발견되는 족족 막아 왔습니다. org-ruby 안에 사용자 문자열을 정규식으로 재주입하는 또 다른 지점이 남았는지는 확인되지 않았습니다 — 가능성은 열려 있습니다. [분석]
Q3 — 조용한/불완전한 재패치는? 전례가 있습니다. CVE-2026-97229는 그 설명문 자체가 “불충분한 정규식 검증”이라 적었고, 탐지 문서도 이를 “93577의 우회(bypass)“라 부릅니다 — 한 번 패치한 것이 뚫렸다는 뜻입니다 [확인]. 따라서 110264의 패치가 근본 수정인지는 “검증 필요”로 보는 게 안전합니다.
선순환 후보(가설). 위 Q2·Q3 신호는 CVE 헌팅 후보로 넘길 가치가 있습니다: ①
org-ruby(및 GitLab 마크업 파이프라인)에서 사용자 문자열을Regexp.new로 재주입하는 다른 콜사이트, ② Onigmo 컴파일 경로에서 16비트 repeat ID 외에 사용자 영향을 받는 다른 고정폭 필드. 둘 다 정적 신호일 뿐이며, 동적 확증 전에는 가설입니다.
대응방안
1) 안전 버전으로 업데이트 (최우선). 위 표의 패치 버전(자신의 라인에 맞는 18.11.13 / 19.0.10 / 19.1.9 / 19.2.9 / 19.3.6 / 19.4.3 / 19.5.1) 이상으로 올리십시오. CVSS 10.0·미인증 RCE이므로 자체 관리형 인스턴스는 지체 없이 적용해야 합니다.
2) 즉시 업데이트가 어려울 때의 차선.
.org파일 렌더링이 꼭 필요하지 않다면, 마크업 렌더에서 Org-mode 처리를 일시 비활성/제한하는 것을 검토하십시오(기능 축소로 공격면 제거).- 공개 프로젝트에서 신뢰할 수 없는 사용자가
.org파일을 올리고 렌더되게 하는 흐름을 한시적으로 차단/검토. - 가능하면 마크업 렌더 워커를 더 강하게 격리(자원·권한 축소)해 OOB write가 전체 프로세스로 번지는 영향을 줄이십시오.
3) 탐지 관점. GitLab 보안팀이 공개 Sigma 규칙(gitlab-security-oss/tldr)을 냈습니다 — Onigmo 네이티브 워커 크래시 시그니처, 그리고 Org-mode 렌더 오류/타임아웃 급증(exceptions_json.log의 extra.class_name: "OtherMarkup") 패턴입니다. 단 앞서 말했듯 성공한 공격은 조용하므로, 이 신호는 “시도의 흔적”을 잡는 보강용으로 쓰고 단독 확정 근거로 삼지 마십시오.
왜 이게 반복되는가 — 논평
이 버그의 뿌리는 메모리 안전(16비트 칸)이 아니라 그 한 층 위에 있습니다. 진짜 출발점은 “사용자가 쓴 문자열을 정규식(코드)으로 만든다”는 설계입니다. 입력이 데이터로만 머물렀다면 16비트 칸은 평생 넘칠 일이 없었습니다. 공격자가 “반복을 몇 개 넣을지”까지 직접 설계할 수 있게 된 건, 입력이 코드가 되는 경계를 넘었기 때문입니다.
그래서 이건 우리가 여러 번 본 계열과 한 뿌리입니다 — 사용자 입력이 질의가 되는 SQL 인젝션, 템플릿이 되는 SSTI(서버 사이드 템플릿 인젝션), 그리고 입력이 필터의 해석을 벌리는 필터 역설계와 같은 사고방식입니다. 방어의 급소도 같습니다: 신뢰할 수 없는 입력을 “실행 가능한 문법”으로 승격시키지 말 것. 꼭 해야 한다면 escape 하거나, 길이·복잡도에 확정적 상한을 두어야 합니다.
흥미로운 지점은 GitLab이 같은 뿌리를 한 달에 세 번 막았다는 사실입니다. 이건 “하나를 고쳐도 같은 뿌리의 형제들이 줄줄이 남는다”는 오래된 교훈을 정확히 보여줍니다 — 취약점 하나를 확증하면 같은 근본원인의 모든 호출부(형제 싱크)를 끝까지 훑어야 하는 이유입니다. 한 콜사이트를 막는 패치는 다음 콜사이트를 막지 못합니다. 근본은 “입력→정규식” 승격 자체이기 때문입니다.
결론
사용자의 한 단어가 정규식이 되고, 그 정규식이 엔진의 16비트 칸을 넘겨, 로그인 없이 서버를 장악했습니다. 한 틈(이 콜사이트)은 고쳐졌지만, “입력을 코드로 만든다”는 사고방식은 CI 규칙에서 두 번, Org-mode에서 한 번 — 다른 자리에서 계속 나왔습니다. 그래서 우리는 도구가 아니라 방법을 남깁니다: 입력이 코드가 되는 경계를 찾고, 그 경계에 확정적 상한을 두는 것.
GitLab 내부 패치 diff가 90일 뒤 공개되면, 이 패치가 근본을 막았는지(Q1) 다시 열어 확인할 값어치가 있습니다.
References / 참고자료
- (재구성한 원 분석) GitLab, “GitLab 19.5.1, 19.4.3, 19.3.6 critical patch release” — 이 글이 재구성한 공식 패치 권고(CVE 설명·영향/안전 버전·CVSS). https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-5-1-released/
- GitLab Security, “Onigmo native worker crash” 탐지 문서 — 세 CVE의 메커니즘·크래시 스택·Sigma 규칙. https://gitlab.com/gitlab-security-oss/tldr
- GitLab Security, “Org mode render error burst” 탐지 문서 — 미인증
.org렌더 접근면·컨트롤러 경로(Projects::BlobController#show)·타임아웃 로그 신호. https://gitlab.com/gitlab-security-oss/tldr/-/raw/main/Detections/platforms/GitLab/Vulnerability%20Detections/org_mode_render_error_burst.md - MITRE CVE-2026-110264 / NVD — CWE-190, CVSS 벡터. https://www.cve.org/CVERecord?id=CVE-2026-110264
- 형제 CVE: CVE-2026-93577, CVE-2026-97229 (CI
rules:*:regexp경로, 원조·우회 관계). org-rubygem 소스 — 키워드 파싱의Regexp.new재주입 구조(개념 확인용). https://github.com/wallyqs/org-ruby
이 글은 위 원 분석들을 우리 관점에서 다시 쓰고 논평을 더한 것입니다. 특정 문장·코드를 그대로 옮기지 않았으며, 자세한 재현 절차나 완성형 페이로드는 싣지 않습니다(랩 공개 윤리).
배지: [확인] = 1차출처(공식 권고·탐지 문서·CVE 레코드·오픈소스)로 확인된 사실 · [분석] = 우리 추론. 이 글은 우리 랩에서 재현하지 않았습니다(재현 배지 없음).