루비 온 레일스(Ruby on Rails, 이하 레일스)가 7월 30일 Active Storage의 치명적 취약점 CVE-2026-66066을 수정한 보안 버전을 배포했다. 신뢰할 수 없는 사용자가 이미지를 올릴 수 있고 Active Storage가 libvips로 이미지를 처리하는 애플리케이션에서는 인증하지 않은 공격자가 서버 프로세스가 읽을 수 있는 임의 파일을 가져갈 수 있다. 수정 버전은 Active Storage 7.2.3.2, 8.0.5.1, 8.1.3.1이다.[1][2]
We released new security versions of Rails to address a critical security bug in Active Storage. Please upgrade your applications.
See the advisory for more information.
레일스 창시자이자 37signals 공동 소유자 겸 최고기술책임자(CTO)인 DHH(David Heinemeier Hansson)는 9분 뒤 이 발표를 인용해 재게시했다. 그는 Active Storage로 파일을 저장하면서 신뢰할 수 없는 사용자의 업로드를 허용하는 애플리케이션은 즉시 업그레이드해야 한다며, 이미지 처리에 사용하는 libvips에 심각한 보안 문제가 있다고 강조했다.[3]
libvips로 신뢰할 수 없는 사용자의 이미지 업로드를 처리하면 영향을 받는다
영향 조건은 두 가지다. Active Storage의 이미지 처리기로 libvips를 사용하고, 신뢰할 수 없는 사용자의 이미지 업로드를 허용해야 한다. config.active_storage.variant_processor = :vips 설정을 쓴 경우가 해당한다. 레일스는 load_defaults 7.0부터 이 설정을 기본값으로 사용한다. libvips가 아니라 ImageMagick 계열 처리기를 쓰면 연구팀이 공개한 이 공격 경로의 영향을 받지 않는다.[2][4]
| 영향받는 Active Storage | 수정 버전 |
|---|---|
< 7.2.3.2 | 7.2.3.2 |
>= 8.0, < 8.0.5.1 | 8.0.5.1 |
>= 8.1, < 8.1.3.1 | 8.1.3.1 |
출처: Ruby on Rails GitHub 보안 권고 [2]
공개 일정을 함께 조율한 Ethiack과 GMO Flatt Security의 연구 자료는 레일스 6과 7 이후 계열의 기본 설정 차이를 설명한다. 레일스 6.0.0부터 6.1.7.10까지는 기본 설정을 바꿔 Active Storage에서 libvips를 사용한 경우 영향을 받을 수 있다. 레일스 7.0.0부터 7.2.3.1까지와 8.0.0부터 8.1.3까지는 기본 이미지 처리기가 libvips이므로 신뢰할 수 없는 사용자의 이미지 업로드 기능이 있으면 영향을 받는다.[4][5]
악성 이미지 업로드로 서버 파일을 읽을 수 있다
공개된 공격 과정은 Active Storage의 직접 업로드 기능을 이용한다. 직접 업로드는 파일 바이트를 확인하기 전에 클라이언트가 선언한 content_type을 데이터베이스에 저장한다. 공격자는 Active Storage 이미지를 표시하는 페이지에서 정상적으로 발급된 variation_key를 수집하고, 조작한 파일을 직접 업로드해 받은 파일 레코드인 blob의 서명된 ID와 조합한 뒤 representations 엔드포인트를 호출한다. variation_key는 위조한 값이 아니어서 서버 시크릿 없이도 검증을 통과한다.[7]
조작한 파일의 앞부분은 MATLAB 5.0 형식이라고 표시해 libvips가 MATLAB 로더를 선택하게 한다. 그러나 파일 안의 별도 버전 필드는 HDF5 기반의 MAT 7.3을 가리킨다. HDF5의 외부 파일 목록 기능에는 경로와 오프셋, 길이를 지정할 수 있어 서버 파일의 바이트를 읽을 수 있다. 읽힌 바이트는 Active Storage 변환 결과 이미지의 픽셀 값으로 저장된다. 공격자는 이 이미지를 받아 파일 내용을 회수한다. 레일스와 libvips, libvips와 libmatio가 서로 다른 위치의 형식 정보를 믿으면서 두 차례의 파일 형식 혼동이 이어진 공격이다.[7]
레일스 보안 권고는 애플리케이션 프로세스가 읽을 수 있는 파일을 공격자가 임의로 가져갈 수 있음을 확인했다고 밝혔다. 공격이 성공하려면 애플리케이션 코드가 별도로 이미지 변환을 요청할 필요는 없다. 공격자가 정상 variation_key를 이용해 직접 요청할 수 있다. GitHub 보안 권고에 등록된 CVSS 4.0 점수는 9.5다. 공격 경로는 네트워크이며 복잡도는 낮다. 다만 앞서 설명한 구성 조건이 필요하고, 공격자가 사전에 확보해야 할 권한이나 사용자 상호작용은 없다.[2][7]
취약점의 직접적인 영향은 파일 읽기다. 하지만 레일스 서버에는 보통 secret_key_base를 비롯해 클라우드 스토리지, 데이터베이스, 외부 서비스 자격증명이 저장돼 있다. 권고는 이런 시크릿이 노출되면 애플리케이션 서버에서 원격 코드를 실행하는 공격으로 이어지거나 연결된 외부 시스템으로 공격 범위가 확대될 수 있다고 설명했다.[2]
패치와 libvips 업데이트, 시크릿 교체를 함께 해야 한다
운영자는 애플리케이션 계열에 맞는 수정 버전으로 Active Storage를 올리고, libvips도 8.13 이상으로 업데이트해야 한다. libvips 8.13 미만에는 안전하지 않은 작업을 차단하는 기능이 없다. 수정된 Active Storage는 libvips 8.13 미만을 쓰는 환경에서 시작 오류를 내고 애플리케이션 실행을 중단한다.[2]
업데이트로 새로운 공격은 막을 수 있지만, 이미 유출됐을 수 있는 시크릿까지 보호되지는 않는다. 영향을 받는 구성으로 운영했다면 secret_key_base, config/master.key 또는 RAILS_MASTER_KEY, 암호화된 자격증명 파일에 저장한 값, S3, GCS, Azure 같은 Active Storage 서비스 키, 데이터베이스 자격증명, 외부 서비스 토큰과 키가 유출됐을 가능성이 있다고 보고 교체해야 한다.[2]
secret_key_base를 바꾸면 현재 세션이 만료돼 사용자가 다시 로그인해야 한다. 암호화 쿠키와 서명 쿠키, 서명된 Global ID, Active Storage URL도 영향을 받는다. 이전 키를 보조 키로 남겨 두면 노출된 시크릿도 계속 유효하다. 레일스 팀은 키 순환을 중간 단계에서만 사용하고 최종적으로 이전 값을 제거하라고 권고했다.[2]
바로 패치하기 어렵다면 libvips를 8.13 이상으로 올려 임시 차단한다
libvips를 8.13 이상으로 올린 다음, 초기화 전에 VIPS_BLOCK_UNTRUSTED 환경변수를 설정하면 안전하지 않은 작업을 차단할 수 있다. ruby-vips 2.2.1 이상을 쓴다면 초기화 코드에서 Vips.block_untrusted(true)를 호출하는 방법도 있다. 두 조치는 Active Storage 수정 버전을 적용하기 전까지만 쓰는 임시 대응이다. WAF도 업로드와 저장 방식에 따라 일부 요청만 막을 수 있어 패치를 대신하지 못한다.[2][4][5]
libvips 8.13 미만에서는 이 우회책을 사용할 수 없다. 이 경우 libvips를 8.13 이상으로 올리거나, 이미지 분석에만 쓰는 ruby-vips 의존성을 제거해 애플리케이션에서 libvips를 빼야 한다. Active Storage를 사용하지 않는 애플리케이션은 불필요한 ruby-vips 의존성을 제거하면 수정 버전에서 발생하는 시작 오류를 피할 수 있다.[2]
공격 흔적은 Active Storage와 오브젝트 스토리지에 남는다
레일스 보안팀은 원래 애플리케이션이 먼저 패치할 시간을 확보하기 위해 공격 경로를 늦어도 8월 28일까지 비공개로 둘 계획이었다. 그러나 여러 연구자가 취약점을 빠르게 역분석해 개념증명 코드를 공개하자 7월 31일 공격 과정과 포렌식 조사 방법을 조기에 공개했다.[6]
알려진 공격은 강도가 다른 세 가지 흔적을 남긴다. 조작한 MAT 파일을 담은 연결되지 않은 blob은 공격 준비가 있었다는 뜻이다. ActiveStorage.track_variants가 켜진 환경에서 해당 blob의 active_storage_variant_records 행은 libvips가 파일을 변환해 읽기 작업이 실행됐음을 보여준다. 그 행에 연결된 변환 결과 이미지는 서버 밖으로 나간 바이트를 픽셀 값으로 보관한다. 공격자가 가져간 내용을 운영자의 오브젝트 스토리지에서 되살릴 수 있다는 점이 일반적인 파일 읽기 취약점과 다르다.[7]
레일스가 공개한 저장소에는 두 가지 에이전트 스킬이 들어 있다. kr2s-was-i-vulnerable은 애플리케이션의 설정과 이력을 살펴 실제 노출 기간을 계산한다. kr2s-was-i-exploited는 그 기간의 Active Storage 데이터에서 조작한 파일을 찾고, 변환 결과 이미지가 남아 있으면 어떤 파일의 어느 바이트가 유출됐는지 복원한다. 이는 애플리케이션에 설치하는 라이브러리가 아니라 에이전트가 코드와 운영 데이터를 읽으며 수행하는 조사 절차이며, 프로덕션에서 실행하는 스캐너는 복사해서 쓰는 단일 Ruby 파일로 제공된다. 도구는 37signals의 실제 포렌식 조사에서 추출됐고 취약점을 신고한 연구자들이 올린 테스트 파일을 찾아냈다. 조사 범위는 Active Storage에서 :vips 처리기와 변환 추적을 사용한 경우이며, 다른 업로드 라이브러리나 자체 libvips 호출 경로는 별도로 확인해야 한다.[6][7]
조사는 가능한 한 빨리 시작해야 한다. 연결되지 않은 blob을 정리하는 예약 작업과 오브젝트 스토리지의 수명 주기 정책은 공격 파일과 변환 결과 이미지를 지울 수 있고, 애플리케이션 로그도 보통 전체 노출 기간보다 먼저 만료된다. ActiveStorage.track_variants가 꺼져 있던 기간이나 공격자가 blob을 연결한 뒤 삭제해 흔적을 지운 경우도 탐지 범위 밖에 남을 수 있다. 따라서 깨끗한 검사 결과는 강한 증거지만 악용이 전혀 없었다는 완전한 증명은 아니다. 운영자는 먼저 패치한 뒤 보존 중인 데이터와 로그를 확보하고, 조사 결과에 따라 유출 가능성이 확인된 시크릿을 교체해야 한다.[7]