CI 서비스의 빌드 로그는 개발자가 무심코 열어보는 화면이다. 그런데 그 로그를 렌더링하는 과정에 심어진 결함 하나가, 로그를 열람한 사람의 계정을 통째로 탈취할 수 있는 통로가 되었다. 한 보안 연구자가 공개한 이번 사례는 오픈소스 코드 호스팅 플랫폼 SourceHut의 CI 서비스인 builds.sr.ht에서 발생했으며, 근본 원인은 ANSI 이스케이프 코드를 HTML로 변환해 주는 파이썬 라이브러리 ansi2html에 있었다. 이 취약점은 로그를 본 브라우저에서 다시 서비스를 공격하는 구조라 자기 전파(wormable) 성격까지 지녔다.
로그가 HTML이 되는 순간
SourceHut은 여러 마이크로서비스로 나뉘어 있고, 빌드 로그 화면은 터미널의 색상 코드나 OSC 8 하이퍼링크 같은 ANSI 이스케이프 시퀀스를 HTML로 바꿔 보여준다. 문제는 이 변환 로직이 입력을 충분히 정제하지 않은 데 있었다. 연구자는 CTF 경험을 살려 ansi2html을 뜯어보다가, OSC 8 링크 구문에 특정 문자열을 끼워 넣으면 의도하지 않은 DOM 트리, 즉 실행 가능한 스크립트가 만들어진다는 사실을 찾아냈다. 즉 빌드 로그 안에 조작된 이스케이프 시퀀스가 한 번 인쇄되기만 하면, 그 로그를 여는 모든 브라우저에서 공격자의 코드가 돌아가게 된다.
공격을 성립시키는 진입점이 여러 개라는 점이 특히 위험하다. 계정 없이도 CI가 켜진 공개 메일링 리스트에 패치를 보내 로그에 문자열을 남길 수 있고, 로그에 출력되는 원격 리소스를 제어하는 방법도 있었다. 물론 자기 계정으로 직접 빌드를 제출할 수도 있지만, 대표 인스턴스에서는 유료 계정이 필요하고 익명 결제 수단이 없어 오히려 흔적이 남는 경로였다.
토큰 한 줄에서 배포 키까지
페이로드가 할 수 있는 일의 범위가 이번 사안의 무게를 결정한다. 빌드 로그 페이지에는 이미 CSRF 토큰이 들어 있어, 스크립트가 이를 읽어내거나 '빌드 재제출' 버튼에 딸린 기존 폼을 그대로 이용해 피해자 이름으로 빌드를 제출할 수 있었다. 관리자가 감염된 로그를 열람하는 순간 공격자가 스스로에게 관리자 권한을 부여하는 시나리오가 가능하고, 더 심각하게는 배포 키에 접근할 수 있었다. 특히 builds.sr.ht에는 sr.ht 자체의 배포 키가 존재해, 대표 인스턴스에서는 플랫폼 전체로 피해가 번질 여지가 있었다. 연구자 표현대로라면 관리자가 자바스크립트를 켠 상태로 감염된 로그를 한 번 보기만 하면 충분했다.
라이브러리를 감사해도 막지 못하는 구조
대응은 두 층위에서 이뤄졌다. SourceHut 측은 ansi2html의 출력을 자동으로 정제하도록 builds.sr.ht를 패치했는데, 이 방식은 효과가 확실한 대신 색상 표시가 사라지는 부작용을 낳았다. 근본적인 처방으로는 Content-Security-Policy에서 unsafe-inline을 제거하는 방안이 거론됐지만, 로그 화면의 스크롤 기능마저 인라인 스크립트에 의존하고 있어 곧바로 적용하기 어려웠다. 상류 라이브러리 차원에서는 파싱 로직을 상태 기반 변환기로 재구조화하는 것이 제시됐다. 취약 버전은 ansi2html 1.7.0 이상 1.9.4 미만, builds.sr.ht 0.40.0 이상 0.105.1 미만으로, builds.sr.ht는 거의 정확히 4년 반 동안 노출돼 있었다.
여기서 실무자가 새겨둘 교훈은, ansi2html을 아무리 꼼꼼히 감사하더라도 버전을 올릴 때마다 다시 검증하지 않으면 SourceHut을 지킬 수 없었다는 점이다. 로그를 화면에 그리는 렌더링 계층은 신뢰 경계가 흐려지기 쉬운 지점이며, 외부 입력이 흘러드는 CI 로그·이슈 본문·커밋 메시지 같은 데이터는 출력 단계에서 반드시 정제 대상으로 다뤄야 한다. 자사 시스템을 점검하려면 원본 빌드 로그에서 OSC 8 관련 이스케이프 시퀀스 패턴을 grep으로 훑어보는 방법이 제시됐다.
점수와 절차라는 또 다른 숙제
연구자는 취약점을 보고하고 수정한 뒤 상류 유지보수 조직인 pycontribs와의 조율 과정도 공개했는데, 이 대목은 오픈소스 공급망의 현실을 드러낸다. ansi2html은 1년 넘게 활동이 없던 상태였고, 두 주요 기여자 중 한 명은 답이 없었으며 다른 한 명과의 협업 끝에 저장소를 되살려 PyPI에 여러 버전을 다시 배포해야 했다. 유지되고 있다고 여겨지던 작은 의존성이 실제로는 방치돼 있는 경우가 흔하다는 점을 보여준다.
CVSS 점수를 둘러싼 논쟁도 곱씹을 만하다. 연구자는 같은 코드 경로에서 비롯된 취약점이라도 이를 쓰는 제품마다 영향이 다른데 하나의 점수로 뭉뚱그리는 방식은 한계가 있다고 지적했다. CVSS 4.0이 '취약한 시스템'과 '후속 시스템'을 구분하기 시작한 것은 진전이지만, XSS처럼 서비스의 결함이 브라우저를 거쳐 다시 서비스를 공격하는 유형에서는 평가가 여전히 논쟁적이다. 초기에 제안한 벡터가 제3자에 의해 조정되기도 했다. 연구자와 프로젝트가 각각 점수를 높이거나 낮출 유인을 갖는다는 지적은, 점수 자체보다 그것이 다운스트림 사용자에게 패치 여부를 판단할 정보를 주는 도구라는 본래 목적을 되새기게 한다.
결국 이번 사례가 남기는 것은 특정 버그의 존재보다, 로그 렌더링과 의존성 관리라는 흔히 간과되는 두 지점이 어떻게 심각한 계정 탈취로 이어지는지에 대한 구체적인 그림이다. 다행히 공개 시점 기준으로 실제 악용 정황은 확인되지 않았고 수정도 완료됐지만, 외부 입력을 화면에 그리는 모든 서비스가 자신의 렌더링 경로를 다시 들여다볼 이유는 충분하다.