박연오

기술 분석 · Jenkins

update-center2 다운로드 페이지의 저장형 XSS

출력 인코딩 누락으로 플러그인 메타데이터가 Jenkins update-center2 다운로드 페이지에서 실행 가능한 콘텐츠로 바뀐 과정과 CVE-2026-84677의 방어적 교훈.

플러그인 배포 권한과 웹사이트 제어 권한

플러그인 유지관리자가 자신의 릴리스에 넣은 표시용 메타데이터가 Jenkins 다운로드 페이지에서 브라우저가 실행하는 콘텐츠로 바뀔 수 있었습니다. update-center2는 플러그인 정보를 바탕으로 이 페이지를 생성합니다. 유지관리자는 해당 플러그인의 설명 문구를 게시할 권한이 있지만, 그 권한이 Jenkins가 운영하는 페이지의 실행 가능한 콘텐츠까지 제어할 권한을 뜻하지는 않습니다. 방문자는 플러그인을 내려받거나 설치하기로 결정하기 전에 이 페이지를 봅니다. Jenkins는 이 저장형 XSS를 CVE-2026-84677로 공개했습니다.

연구에서 확인하려던 질문은 특정 플러그인을 배포하는 통상적인 권한이 중앙에서 생성하는 웹사이트에 대한 제어 권한으로 넘어갈 수 있는지였습니다. 당시 로컬 검증에서는 공식 update-center2 3.18.3 생성기와 브라우저를 사용했습니다. 아래에서 다루는 3.18.4 수정 내용은 이후 공개된 릴리스 소스를 검토한 결과이지, 새로 수행한 런타임 테스트의 결과가 아닙니다.

배포와 화면 출력이 만나는 지점

Jenkins의 배포 안내에 따르면 특정 플러그인을 배포하려면 그 플러그인에 대한 권한이 필요합니다. 제가 살펴본 출발점도 플러그인 하나의 메타데이터를 제어할 수 있는 권한이었습니다. update-center2, 업데이트 사이트의 서명 키, Jenkins 웹 인프라에 대한 권한을 전제하지 않았습니다. 당시 로컬 검사에서 통상적인 릴리스 검증과 패키징은 의도적으로 지정한 표시 값을 받아들였습니다. 배포 과정에서 그 값이 거부되었다면, 일반 유지관리자의 권한만으로 이 경계를 넘을 수 있다는 주장을 뒷받침하기 어려웠을 것입니다. 테스트 콘텐츠를 Jenkins에 업로드하지는 않았습니다.

게시할 값을 받아들이는 일과 그 값을 안전하게 화면에 표시하는 일은 다릅니다. 공개된 3.18.3의 IndexHtmlBuilder.close 소스에서는 플러그인이 제공한 제목을 생성 HTML에 그대로 대입했습니다.

.replace("{{ title }}", title)

핵심은 유지관리자가 메타데이터를 제공할 수 있었느냐가 아닙니다. 메타데이터 제공은 플러그인 배포의 정상적인 부분입니다. 문제는 중앙에서 생성한 페이지가 그 데이터를 표시할 때 브라우저에서 실행 가능한 의미를 갖게 했다는 점입니다.

당시 대조 실험으로 확인한 것

소스 검토만으로는 위험한 출력 방식을 확인할 수 있을 뿐, 브라우저에서 실제로 어떤 일이 일어났는지는 확인할 수 없습니다. 당시 update-center2 3.18.3에 대한 로컬 브라우저 검증에서는 지정한 표시 값으로 생성한 다운로드 페이지들을 비교했습니다. 인코딩하지 않고 표시한 경우에만 브라우저에서 무해한 표식이 관찰됐습니다. 다음 표는 보존된 결과를 요약한 것이며, 재현 절차나 이 글을 위해 새로 수행한 실험은 아닙니다.

로컬 비교 조건 브라우저 관찰 결과 구별해 주는 점
게시자가 지정한 표시 값을 인코딩 없이 출력 무해한 표식이 나타났습니다. 생성된 페이지에서 단순히 특이한 소스 텍스트가 나온 것에 그치지 않고 브라우저 동작이 발생했습니다.
일반적인 평문 값 표식이 나타나지 않았습니다. 페이지 생성이나 브라우저 방문 자체가 그 효과를 일으킨 것은 아니었습니다.
표시 값을 HTML용으로 인코딩 표식이 나타나지 않았습니다. 메타데이터를 텍스트로 처리하면 관찰된 효과가 사라졌습니다.

로컬 배포 검사로는 다른 가능성, 즉 페이지 생성 전에 정상적인 릴리스 처리 과정에서 해당 메타데이터가 거부될 수 있다는 의문을 확인했습니다. 일회용 환경에서는 입력이 받아들여졌지만, 이것이 Jenkins 인프라에 악성 콘텐츠가 게시된 적이 있음을 입증하지는 않습니다. Jenkins 보안 공지는 플러그인 이름, 설명, 버전 메타데이터 전반의 문제를 다룹니다. 당시 제 브라우저 관찰은 그중 표시 이름에 관한 것이었습니다.

영향과 확인되지 않은 범위

입증된 효과는 로컬에서 생성한 다운로드 페이지에서의 스크립트 실행입니다. 플러그인을 설치하지 않고도 웹페이지 콘텐츠의 경계를 넘었으며, 이런 실행은 방문자에게 보이는 내용을 바꿀 수 있습니다. 악성 콘텐츠를 담은 실제 공개 릴리스, 실제 방문자의 페이지 이동, 아티팩트 설치, 자격 증명 접근, Jenkins 컨트롤러에 대한 영향은 검증하지 않았습니다. 특히 다운로드 페이지에서 브라우저 코드가 실행됐다는 사실만으로 Jenkins 컨트롤러의 오리진에서 스크립트가 실행됐다거나 관리자 권한이 침해됐다고 볼 수는 없습니다.

Jenkins는 이 문제를 CVSS 3.1 기준 중간 5.4로 평가하고, 3.18.3 및 이전 버전이 영향받는다고 밝힙니다. 당시 런타임 근거는 3.18.3에 관한 것입니다. 그보다 넓은 버전 범위는 벤더의 평가이지, 제가 이전 버전을 모두 시험했다는 뜻이 아닙니다. 여기서 검토한 소스만으로 최초 영향 버전도 추정하지 않습니다.

공개된 수정 내용 살펴보기

Jenkins에 따르면 update-center2 3.18.4는 플러그인 이름, 설명, 버전 메타데이터를 이스케이프합니다. 공개된 3.18.4의 IndexHtmlBuilder.close 소스에서는 같은 출력 지점에서 제목을 인코딩한 뒤 대입합니다.

.replace("{{ title }}", escapeXml(title))

같은 릴리스 소스는 설명, 버전 표시 문구와 다른 생성 필드도 이스케이프합니다. 메타데이터가 HTML이 되는 지점의 처리가 바뀐 것입니다. 게시자는 여전히 값을 제공하지만, 생성기는 그 값을 데이터로 표시합니다. 이 글을 위해 공개된 3.18.3과 3.18.4 릴리스 소스를 비교했습니다. 이 비교는 배포된 수정의 작동 원리를 뒷받침하지만, 수정된 릴리스의 브라우저 실행 결과는 아닙니다. 3.18.4에 대한 독립적인 런타임 검증은 다시 수행하지 않았습니다.

회귀 테스트에서는 이스케이프 함수만이 아니라 실제 생성된 페이지를 확인하는 것이 유용합니다. 다음은 제안하는 테스트이며, 실행 결과나 업스트림 테스트 범위에 대한 주장이 아닙니다.

제안하는 테스트 기대되는 동작
일반적인 플러그인 이름과 설명 문서 제목, 화면의 제목, 설명에서 읽기 쉽게 표시돼야 합니다.
마크업처럼 보이는 이름이나 설명 템플릿에서 표시되는 모든 위치에서 실행되지 않는 텍스트로 남아야 합니다.
특이한 버전 메타데이터 생성된 버전 목록과 속성에서 텍스트로 남아야 합니다.

남은 검증 과제는 공개된 3.18.4 생성기로 이 대조 조건을 실행해 값이 정상적으로 표시되고 브라우저에서 스크립트 등 활성 콘텐츠는 실행되지 않음을 확인하는 것입니다. 당시 로컬 관찰과 이후의 소스 비교는 서로 다른 질문에 답합니다. 어느 쪽도 검증하지 않은 공개 사이트나 Jenkins 컨트롤러에 대한 영향까지 주장하는 근거로 넓혀서는 안 됩니다.