[하루한줄] CVE-2026-110264 : GitLab의 Integer Overflow로 인한 Remote Code Execution

URL

Target

  • GitLab CE/EE 7.0 ~ 18.11.12
  • GitLab CE/EE 19.0.x < 19.0.10
  • GitLab CE/EE 19.1.x < 19.1.9
  • GitLab CE/EE 19.2.x < 19.2.9
  • GitLab CE/EE 19.3.x < 19.3.6
  • GitLab CE/EE 19.4.x < 19.4.3
  • GitLab CE/EE 19.5.x < 19.5.1

Explain

CVE-2026-110264는 GitLab에서 사용하는 Ruby 정규표현식 엔진 Onigmo의 Integer Overflow 취약점입니다.
정규표현식의 반복 구문에 부여되는 ID가 16비트 정수 범위를 초과하면서 잘못된 값으로 저장되고, 이를 배열 인덱스로 사용하는 과정에서 Out-of-bounds Write가 발생합니다.

Background

GitLab은 org-ruby 라이브러리를 사용하여 .org 문서를 렌더링합니다. .org 문서에서는 #+TODO: 를 통해 사용자 지정 TODO 키워드를 정의할 수 있습니다.

org-ruby의 headline.rb에는 다음과 같이 키워드를 정규표현식으로 처리하는 코드가 존재합니다.

@headline_text.sub!(Regexp.new("^#{@keyword}\s*"), "")

이때 @keyword를 이스케이프하지 않고 Regexp.new() 에 전달하므로, 키워드에 포함된 정규표현식 문법이 그대로 해석됩니다.

공개 프로젝트에 공격자가 제어하는 .org 파일이 존재하고 GitLab이 이를 렌더링하면, 별도의 인증 없이 취약한 정규표현식 처리 과정에 도달할 수 있습니다.

Root Cause

Onigmo는 정규표현식의 반복 구문마다 ID를 부여합니다. ID는 int로 관리되지만, 컴파일된 명령에 저장할 때는 short int인 MemNumType으로 변환됩니다.

// regint.h
typedef short int MemNumType;

// regcomp.c - add_mem_num()
MemNumType n = (MemNumType )num;
BBUF_ADD(reg, &n, SIZE_MEMNUM);

16비트 signed short 환경에서는 ID가 32,767을 초과하면 음수로 변환될 수 있습니다. 그러나 취약한 버전에는 이 값의 범위를 검사하는 코드가 존재하지 않습니다.

이후 정규표현식을 실행할 때 regexec.c 의 OP_REPEAT 에서 저장된 ID를 읽어 배열 인덱스로 사용합니다.

CASE(OP_REPEAT)  MOP_IN(OP_REPEAT);
  {
GET_MEMNUM_INC(mem, p);    /* mem: OP_REPEAT ID */
GET_RELADDR_INC(addr, p);
			
STACK_ENSURE(1);
repeat_stk[mem] = GET_STACK_INDEX(stk);
STACK_PUSH_REPEAT(mem, p);
			
if (reg->repeat_range[mem].lower == 0) {
  CHECK_MATCH_CACHE;
  STACK_PUSH_ALT(p + addr, s, sprev, pkeep);
}
  }

repeat_stk는 OnigStackIndex 타입의 배열로, 각 반복 구문(OP_REPEAT)이 백트래킹 스택의 어느 위치까지 진행되었는지를 저장합니다.
이 배열은 원래 alloca()로 스레드 스택에 할당되지만, 정규표현식의 그룹 수가 MAX_PTR_NUM을 초과하면 스택 오버플로를 피하기 위해 xmalloc()으로 힙에 할당됩니다.

여기서 mem이 음수가 되면 repeat_stk 의 시작 주소보다 앞쪽에 값이 기록되어 OOB Write가 발생합니다.

정규표현식을 컴파일하면서 반복 ID가 잘못 저장되고, 매칭 과정에서 해당 ID를 배열 인덱스로 사용하면서 메모리 손상이 발생합니다.

PoC

공개 PoC 저장소에서는 GitLab CE 19.3.1을 대상으로 .org 파일을 렌더링할 때 발생하는 처리 지연과 Timeout::Error를 확인했습니다.

#+TODO: 키워드에 반복되는 정규표현식 구문을 포함시킵니다. 다음은 반복 패턴을 생성하는 코드 일부입니다.

UNIT_LOUD = "[[:word:]]*"
REPEATS_DEFAULT = 35000

def build_keyword(repeats=REPEATS_DEFAULT, unit=UNIT_LOUD):
    return unit * repeats

생성한 키워드를 포함한 .org 문서를 GitLab에서 렌더링하여 정상 문서 응답 시간을 비교하였고, 지연된 렌더링 시간이 확인되었습니다.

그리고, 서버의 exceptions_json.log 에서도 .org 렌더링 과정에서 발생한 Timeout::Error가 확인되었습니다.

Patch

Ruby에서는 2026년 4월 PR #16801을 통해 반복 ID과 널 체크 ID의 범위를 검사하는 코드를 추가했습니다. regcomp.c의 compile_range_repeat_node()에 추가된 코드는 다음과 같습니다.

@@ -703,6 +704,7 @@ compile_range_repeat_node(QtfrNode* qn, int target_len, int empty_info,
  if (r) return r;
  r = add_mem_num(reg, num_repeat); /* OP_REPEAT ID */
  reg->num_repeat++;
+ if ((MemNumType)reg->num_repeat <= 0) return ONIGERR_TOO_MANY_RANGE_REPEAT;
  if (r) return r;
  r = add_rel_addr(reg, target_len + SIZE_OP_REPEAT_INC);
  if (r) return r;

MemNumType으로 변환한 값이 0 이하가 되면 오류를 반환하고 컴파일을 중단합니다. num_null_check에도 동일한 검사가 추가되었으며, 정규표현식 컴파일 시 RegexpError가 발생하도록 수정되었습니다.

해당 수정은 당시 Ruby 메인 브랜치에만 적용되어 GitLab에서 사용하던 Ruby 3.3, 3.4, 4.0에는 포함되지 않았습니다.

이후 GitLab은 해당 패치를 번들 Ruby에 백포트하고, 2026년 10월 21일 다음 버전의 보안 업데이트를 배포했습니다.

  • GitLab CE/EE: 18.11.13, 19.0.10, 19.1.9, 19.2.9, 19.3.6, 19.4.3, 19.5.1
  • Ruby: 3.3.13, 3.4.12, 4.0.9

GitLab은 Ruby 3.3, 3.4, 4.0에 각각 적용할 패치 파일을 추가했으며, config/software/ruby.rb에서 해당 패치를 적용하도록 변경했습니다.

한편 org-ruby에서도 PR #112를 통해 사용자 지정 TODO 키워드에 Regexp.escape()를 적용하도록 수정했습니다.

@@ -97,7 +97,7 @@ def parse_keywords
      words = @headline_text.split
      if words.length > 0 && words[0] =~ re then
        @keyword = words[0]
-       @headline_text.sub!(Regexp.new("^#{@keyword}\s*"), "")
+       @headline_text.sub!(Regexp.new("^#{Regexp.escape(@keyword)}\s*"), "")
      end
    end
  end

이 수정은 TODO 키워드에 포함된 정규표현식 특수문자를 일반 문자열로 처리하도록 합니다. GitLab의 Ruby 백포트와는 별도로 진행된 org-ruby의 수정입니다.

Reference



본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.