GitLab 만 올리고 Ruby 는 그대로, Alpine 재구축 사례로 본 점검 순서
GitLab 을 Ruby 와 따로 설치해 돌린다면, GitLab 을 수정 버전으로 올려도 CVE-2026-97229 는 남는다. GitLab 의 수정은 자기가 배포하는 패키지와 컨테이너 이미지 속 Ruby 에 넣은 패치였고, 소스 설치나 자체 빌드 이미지는 10월 23일 나온 Ruby 수정 릴리스까지 받아야 막힌다. Alpine Linux 는 이 차이를 놓쳐 GitLab 을 올린 뒤에도 닷새 더 취약했고, 그사이 공격자가 다시 들어오려 했다.
01 앞 글 뒤로 실제 공격 사례와 Ruby 업스트림 수정이 새로 나왔다
GitLab CVSS 10.0 정규식 RCE, 직전 긴급 패치로는 막히지 않는 이유와 버전표 뒤로 새로 나온 것은 두 가지다. 하나는 Ruby 가 10월 23일 4.0·3.4·3.3 세 라인에 낸 Onigmo 수정 릴리스다. 앞 글을 쓸 무렵에는 Ruby 정식 릴리스에 이 수정이 없어서, 소스 설치 운영자에게는 올릴 버전이 없었다. 다른 하나는 Alpine Linux 가 10월 28일 올린 사고 보고로, 자기 GitLab 서버가 10월 20~21일 CVE-2026-97229 악용 시도를 받았다는 내용이다.
이 글은 Alpine 사례가 드러낸 'Ruby 를 따로 쓰는 GitLab' 의 빈틈과 그 점검 순서만 다룬다. GitLab 라인별 수정 번호는 앞 글의 표를 그대로 쓰면 된다. 아래에서 버전은 라인과 패치 번호로 적는다. '19.3 라인 패치 5' 는 19.3 라인에서 패치 번호가 5 인 릴리스를 뜻한다.
02 Alpine 의 GitLab 은 두 번 올리고도 보름 동안 열려 있었다
GitLab 이 CVE-2026-97229 수정을 낸 날은 10월 12일이다. Alpine 은 CVE 가 공개된 13일에 바로 업그레이드했지만 목표를 잘못 골라, 수정이 든 19.3 라인 패치 5 가 아니라 패치 4 로 올렸다. 22일에 패치 6 으로 다시 올렸을 때는 Ruby 가 발목을 잡았다. Alpine 이 직접 만든 GitLab 이미지에는 GitLab 이 손본 Ruby 대신 업스트림 그대로의 Ruby 가 들어 있었다. 그래서 서버는 27일 외부 접근을 막을 때까지 취약했다.
| 날짜 | 일어난 일 |
|---|---|
| 10월 12일 | GitLab 이 19.4·19.3·19.2 라인 수정 릴리스 공개 |
| 10월 13일 | CVE 공개. Alpine 이 패치 5 대신 패치 4 로 잘못 올림 |
| 10월 20일 | 새로 가입한 계정이 서버를 살피기 시작 |
| 10월 21일 | 본 공격. 조작한 CI 설정으로 작업자 프로세스가 8번 죽음(그중 4번은 익스플로잇 중간 단계까지 감). 502 오류와 CI 작업 실패 |
| 10월 22일 | 19.3 라인 패치 6 으로 올림. Ruby 는 그대로 |
| 10월 23일 | 공격 계정 차단. 같은 날 Ruby 업스트림 수정 릴리스 |
| 10월 24일 | 저장소·패키지·21일 릴리스를 GitLab 밖 기록과 대조 |
| 10월 25~26일 | 공격자 재진입 시도 실패. 26일에 서버가 여전히 취약함을 확인 |
| 10월 27일 | 개발자·빌드 서버 말고는 접근 차단, 재구축 시작 |
| 10월 28일 | 재구축 완료. 전체 비밀번호 초기화, GitLab 비밀 키 교체 |
공격자는 새로 만든 계정 하나로 움직였다. Alpine 은 그 공격자가 Alpine 의 GitLab 컨테이너 이미지를 내려받은 점을 들어, 익스플로잇을 Alpine 빌드에 맞춰 조정했을 수 있다고 봤다. 이번 공격자는 계정을 만들어 썼지만, GitLab 은 이 취약점의 CVSS 벡터에 '권한 필요 없음(PR:N)' 을 매겨 로그인 없이도 가능한 공격으로 평가했다. 회원 가입을 닫는 것만으로 충분하다고 볼 근거는 없는 셈이다.
03 수정이 Ruby 안에 있어서 GitLab 버전만으로는 판단이 안 된다
Onigmo 는 Ruby 에 내장된 정규식 엔진이다. Ruby 공지에 따르면 조작한 패턴을 컴파일하는 동안 메모리가 깨질 수 있고, GitLab 서버에서는 이것이 원격 코드 실행으로 이어질 수 있다. CVE-2026-97229 에 해당하는 버그는 컴파일된 패턴 크기의 정수 오버플로다. GitLab 은 CI 설정에 적힌 정규식을 평가하다 이 버그를 밟는다. CVSS 는 GitLab 이 매긴 10.0 이다.
닿는 조건은 Ruby 공지가 좁혀 두었다. 공격자가 고른 문자열로 정규식을 컴파일할 때, 곧 Regexp.new·Regexp.union·Regexp.compile 에 넘기거나 /#{input}/ 처럼 리터럴에 끼워 넣을 때만 문제가 된다. 개발자가 써 둔 정규식만 쓰는 앱은 해당하지 않는다. 다만 Regexp.timeout 은 이 버그를 막지 못하고, 짧은 패턴으로도 터지는 경우가 있어 길이 제한도 믿을 방어가 못 된다.
GitLab 이 수정을 어디에 실었는지 보면 버전만으로 안 되는 이유가 보인다. 10월 12일 패치 노트의 버그 수정 목록에는 Ruby 3.3·3.4 용 정규식 패치를 더하는 항목이 Omnibus 와 CNG(Cloud Native GitLab 이미지 빌드) 쪽에 있다. 10월 21일 릴리스에도 3.3·3.4·4.0 용 반복 ID 오버플로 패치가 같은 자리에 들어갔다. 애플리케이션 코드가 아니라, GitLab 이 직접 빌드해 싣는 Ruby 를 고친 셈이다.
| 설치 방식 | Ruby 를 대는 쪽 | 할 일 |
|---|---|---|
| GitLab.com·GitLab Dedicated | GitLab | 없음 |
| 공식 Linux 패키지(Omnibus)·공식 이미지 | GitLab 이 패치해 번들 | GitLab 만 수정 버전으로 |
| 소스 설치 | 운영자가 설치한 Ruby | GitLab 과 Ruby 둘 다 |
직접 또는 배포판이 만든 이미지(예: alpinelinux/gitlab) | 이미지 안의 배포판 Ruby | 둘 다 올리고 이미지 다시 빌드·받기 |
Ruby 쪽에서 무엇이 언제 고쳐졌는지는 Ruby 공지의 표를 라인별로 옮기면 이렇다.
| GitLab CVE | Ruby 쪽 버그 | 4.0 | 3.4 | 3.3 |
|---|---|---|---|---|
| CVE-2026-89078 | 교대 연산자 뒤 파싱이 실패할 때 이중 해제 | 첫 릴리스부터 | 패치 12 | 패치 13 |
| CVE-2026-93577 | 컴파일된 패턴 크기의 정수 오버플로 | 패치 8 | 패치 12 | 패치 13 |
| CVE-2026-97229 | 바로 위와 같은 버그 | 패치 8 | 패치 12 | 패치 13 |
| CVE-2026-110264 | 반복이 많은 패턴에서 내부 ID 오버플로 | 패치 9 | 패치 12 | 패치 13 |
| 넷 모두 막는 최소 | — | 패치 9 | 패치 12 | 패치 13 |
4.0 라인은 패치 8 에 머물면 CVE-2026-110264 가 남는다. 3.3 은 원래 보안 수정만 받는 단계인데, GitLab 이 CVE 를 매긴 버그라 예외로 정규식 수정을 넣었다고 Ruby 는 밝혔다. 3.2 는 2026년 4월 1일 지원이 끝나 이번 수정 릴리스가 없다. Ruby 공지는 3.2 이하가 영향을 받는지 따로 적지 않았지만, GitLab 은 CVE-2026-110264 의 영향 범위를 GitLab 7.0 부터로 잡았다. GitLab 소스 설치 문서는 10월 30일 현재 17.5 이후 필요한 Ruby 를 3.2 로 적고 있으니, 그 최소 요건에 맞춰 둔 서버라면 라인을 3.3 이상으로 옮기는 편이 낫다.
Ruby 는 이번 수정에 자기 CVE 번호를 요청하지 않았고, 당분간 GitLab 의 CVE 번호로 추적하라고 했다. 이미지 스캐너나 배포판 보안 트래커에서 Ruby 패키지를 찾을 때도 GitLab CVE 번호로 찾아야 한다는 뜻이다. 실제로 OSV 의 Alpine 항목(ALPINE-CVE-2026-97229)은 gitlab 이 아니라 ruby 패키지를 영향 대상으로 올려 두었다.
04 내 GitLab 이 어느 쪽인지 네 단계로 가린다
- 설치 방식부터 본다. 공식 Linux 패키지나 공식 이미지라면 GitLab 버전만 앞 글 표와 맞추면 된다.
gitlab:env:info로 GitLab 버전과 Ruby 버전을 함께 본다.- Ruby 를 따로 쓰면 Ruby 번호가 판단 기준이다.
- 컨테이너는 태그 이름이 아니라 받은 시점과 digest 로 판단한다.
# 공식 Linux 패키지(Omnibus)
sudo gitlab-rake gitlab:env:info
# 소스 설치 — GitLab 설치 디렉터리에서
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production출력에서 System information 아래 Ruby Version: 줄과 GitLab information 아래 Version: 줄을 본다. 공식 패키지라면 Ruby 줄의 번호는 기준이 아니다. GitLab 이 번들 Ruby 에 패치를 직접 넣었으니, GitLab 버전이 수정 버전 이상이면 된다. 소스 설치는 반대로, Ruby 번호가 위 표의 '넷 모두' 행보다 낮으면 GitLab 이 최신이어도 취약하다.
컨테이너는 같은 태그라도 내용이 바뀔 수 있다. Alpine 은 alpinelinux/gitlab 의 19.3 라인 패치 6 태그를 10월 27일 Ruby 3.3 패치 13 을 넣어 다시 게시했고, 그보다 먼저 받은 같은 태그와 더 오래된 태그는 모두 취약하다고 밝혔다. 태그 이름으로는 둘을 구별할 수 없으니 생성 시각과 digest, 컨테이너 안의 Ruby 를 직접 본다.
# 받아 둔 이미지의 생성 시각과 digest
docker image inspect --format '{{.Created}} {{.RepoDigests}}' "alpinelinux/gitlab:$TAG"
# 실행 중인 컨테이너 안의 Ruby
docker exec "$CONTAINER" ruby -v10월 30일 Docker Hub 의 alpinelinux/gitlab 페이지에는 19.3 라인 패치 7 태그도 새로 보였다. 페이지에 Ruby 버전은 적혀 있지 않으니, 어느 태그든 받은 뒤 ruby -v 로 확인하는 편이 낫다.
Alpine 패키지의 Ruby 를 쓰는 서버나 이미지라면 apk upgrade 로 수정판을 받는다. Alpine 이 밝힌 브랜치별 수정 Ruby 는 다음과 같다.
| Alpine 브랜치 | 수정이 든 Ruby |
|---|---|
| 3.21 | 3.3 라인 패치 13 |
| 3.22·3.23·3.24·edge | 3.4 라인 패치 12 |
GeekNews 가 정리한 Hacker News 토론에는, 태그는 버전이 아니니 이미지를 SHA digest 로 고정하고 바뀌면 알림을 받자는 의견이 있었다. 작은 팀이라면 이 방식이 되돌리기에도 낫다. 문제가 생기면 직전 digest 로 돌아가면 되고, '다시 받으라' 는 공지가 나오면 내 digest 가 새것인지 바로 비교할 수 있다.
05 흔적이 모자라자 Alpine 은 침해로 보고 새로 지었다
Alpine 이 본 증상은 작업자 프로세스 비정상 종료 8번, 502 오류, CI 작업 실패였다. GitLab 도 10월 12일 패치 노트에 이 CVE 의 악용 시도를 잡는 탐지 규칙 여섯 개를 실었는데, 그중에 'Onigmo native worker crash' 와 'ci regexp guard rejection' 이 있다. Alpine 사례에서 밖으로 드러난 신호도 결국 정규식을 처리하던 작업자가 죽는 일이었다.
코드가 실행된 흔적은 나오지 않았다. 시간 지연 검사에서 이상한 지연이 없었고, 예상 밖의 파일·키·훅·프로세스도 없었으며, 확인할 수 있는 웹·API 응답 가운데 비공개 데이터를 내준 것도 없었다. 그래도 Alpine 은 '안 뚫렸다' 고 결론 내리지 않았다. 성공한 익스플로잇은 프로세스를 죽이지도 웹 로그를 남기지도 않을 수 있다. 게다가 공격 당일의 백그라운드 작업 로그와 이전 컨테이너의 파일시스템 일부가 사라졌고, 바깥으로 나간 연결은 Alpine 도 호스팅 업체도 기록하지 않았다.
공격이 성공했다면 새어 나갔을 수 있는 것으로 Alpine 은 다음을 꼽았다.
- 전체 사용자의 이메일·이름·IP
- 비밀번호 해시와 2FA 비밀값
- CI/CD 변수와 러너 토큰
- 웹훅·통합·푸시 미러 자격 증명, 해시로 저장된 개인 액세스 토큰
- 비공개 저장소, 기밀 이슈와 머지 요청
재구축하면서 Alpine 은 모든 사용자 비밀번호를 초기화하고 GitLab 비밀 키를 바꿨다. 다른 곳에서 샌 비밀번호로 자동 로그인을 시도한 기록도 봤다며, 같은 비밀번호를 다른 서비스에서 썼다면 그쪽도 바꾸라고 했다.
공급망은 GitLab 밖의 기록으로 따로 확인했다. 10월 20~22일 들어온 aports 커밋을, 공격자 손이 닿지 않는 개발자 복제본과 빌더 45대의 기록에 맞춰 봤다. 10월 20~24일 빌드한 패키지와 10월 21일 안정 릴리스 네 개(ISO 와 공식 alpine 이미지의 바탕인 minirootfs 포함)도 검증된 커밋에서 나왔다. 패키지 서명 키는 GitLab 서버에 둔 적이 없다. 다만 GitLab 은 문을 닫은 27일까지 취약했으므로, Alpine 은 같은 검증을 그날까지 넓히는 중이다.
Alpine 이 밝힌 재발 방지책은 넷이다.
- 치명 등급 GitLab 보안 릴리스는 하루 안에 적용하고, 그때마다 Ruby 같은 구성 요소 패치가 따로 필요한지 본다.
- GitLab 호스트의 바깥 연결과 DB 연결을 기록한다.
- 로그를 14일보다 길게 보관한다.
- 릴리스 태그에 서명해 GitLab 없이도 검증할 수 있게 한다.
06 Ruby 를 직접 챙기는 GitLab 이라면 지금 할 일은 Ruby 를 올리는 것이다
대상은 셀프호스팅 GitLab 을 소스로 설치했거나, Dockerfile 에서 배포판 Ruby 위에 GitLab 을 얹은 이미지를 쓰는 팀이다. 이런 팀이 GitLab 보안 공지만 보고 끝내면 Alpine 의 10월 22일과 같은 상태가 된다. 지금 Ruby 를 위 표의 '넷 모두' 기준 이상으로 올리고, 이미지라면 다시 빌드한 뒤 컨테이너 안에서 ruby -v 로 확인한다. 공식 Linux 패키지·공식 이미지·GitLab.com 을 쓰면 Ruby 쪽으로 따로 할 일은 없고, GitLab 버전만 앞 글 표에 맞추면 된다.
GitLab 밖의 Ruby 서비스도 같은 버그에 닿을 수 있다. 사용자 입력으로 정규식을 만드는 자리가 있다면 Ruby 를 올리는 것이 답이고, 당장 못 올리면 그 자리에서 사용자 입력을 정규식으로 컴파일하지 말라는 것이 Ruby 의 권고다. Regexp.timeout 만 걸어 둔 상태는 방어로 치지 않는 편이 맞다. 그런 자리는 아래처럼 먼저 추려 볼 수 있다.
# 사용자 입력이 정규식으로 컴파일될 수 있는 후보 찾기
grep -rnE 'Regexp\.(new|union|compile)|/#\{' app lib10월 12일 이후 인터넷에 열린 GitLab 이 GitLab 이나 Ruby 쪽 수정 없이 돌았다면, 그 기간의 작업자 비정상 종료 기록부터 찾는다. 기록이 있는데 바깥 연결 로그도 이전 컨테이너도 남아 있지 않다면, Alpine 처럼 침해로 보고 다시 짓는 편이 낫다. 이때는 비밀번호 초기화와 GitLab 비밀 키 교체에 더해, 노출 목록에 든 CI/CD 변수 속 클라우드·배포 자격 증명과 러너 토큰도 바꾸는 편이 낫다. 업그레이드할 때 이전 컨테이너를 지우지 말고 멈춰만 두면, Alpine 이 잃은 파일시스템 증거를 남길 수 있다.
공식 alpine 베이스 이미지만 쓰는 팀은 지금 할 일이 없다. Alpine 은 10월 21일 릴리스와 24일까지 빌드한 패키지에서 변조를 찾지 못했고, 서명 키는 애초에 GitLab 서버에 없었다. 다만 검증을 27일까지 넓히는 중이니 후속 공지는 지켜볼 만하다. Alpine 에 기여하느라 gitlab.alpinelinux.org 계정이 있다면, 재개 후 로그인 화면의 'Forgot your password?' 로 새 비밀번호를 정하면 된다. 자세한 절차는 Alpine 이 따로 안내하겠다고 했다.
결정하는 쪽에서 보면, Ruby 를 직접 관리하는 설치에는 GitLab 보안 릴리스마다 구성 요소 패치까지 확인하는 일이 따라붙는다. 그 손이 없다면 공식 패키지로 옮기는 쪽이 길게 보면 가볍지만 이전 작업이 따로 드니, 이번 대응은 Ruby 를 올리는 것으로 먼저 끝내고 이전은 나중에 따로 따지는 편이 낫다.
07 자주 묻는 질문
소스 설치 문서는 Ruby 3.2 를 적는데, 3.3 으로 옮겨도 되나
3.2 에는 이번 수정 릴리스가 없으니 남는 길은 라인을 올리는 것뿐이다. GitLab 이 10월 12일 공식 패키지·이미지용으로 낸 정규식 패치도 Ruby 3.3·3.4 용이었다. GitLab 이 직접 빌드해 싣는 Ruby 가 이미 그 라인이라는 뜻이다. 그래도 문서 요건과 다른 라인이니, 운영 서버에 바로 바꾸기보다 같은 GitLab 버전으로 스테이징에서 먼저 돌려 보는 편이 안전하다.
노드를 여러 대로 나눈 GitLab 은 어디서 확인하나
GitLab 문서는 gitlab:env:info 를 GitLab Rails 가 도는 노드에서 실행하라고 한다. 다른 노드에서 돌리면 PostgreSQL 소켓 오류가 날 수 있다. Ruby 를 따로 설치한 구성이라면 Ruby 도 노드마다 따로 깔리니, Rails·Sidekiq 를 돌리는 노드마다 ruby -v 를 보는 편이 확실하다.