CVE-2026-84939 (Apache FreeMarker 경로 탐색) 분석

분류: CVE · 웹·서버 · 경로탐색 · CVE-2026-84939 · [표준]

템플릿 엔진에 “이 언어로 표시해 주세요”라고 부탁하는 값 하나가, 알고 보니 파일 경로의 일부로 그대로 이어붙여지고 있었습니다. Apache FreeMarker는 사용자 언어(locale)에 맞는 템플릿을 자동으로 찾아주는 기능에서, 그 언어 이름을 검증 없이 경로에 삽입했습니다. 언어 이름 자리에 ..를 채워 넣을 수 있다면, 그건 더 이상 언어 이름이 아니라 경로 탈출 명령이 됩니다.

개요

Apache FreeMarker는 자바 애플리케이션에서 널리 쓰이는 템플릿 엔진 라이브러리입니다. HTML 페이지·이메일·설정 파일 등을 템플릿과 데이터로 조합해 생성하는 데 쓰이며, 자체적으로는 서버가 아니라 다른 애플리케이션에 내장되는 컴포넌트입니다. 이번 취약점은 이 라이브러리의 “지역화(localized) 템플릿 조회” 기능에 있습니다. 사용자의 지역(locale)에 맞춰 template_ko_KR.ftl처럼 이름이 변형된 템플릿을 자동으로 찾아주는데, 이때 사용되는 locale 값 자체가 경로 조작에 악용될 수 있습니다.

공격이 성립하려면 조건이 둘 필요합니다. 첫째, “지역화 조회(localized lookup)” 설정이 켜져 있어야 하는데, 이 값은 기본값이 활성화입니다. 둘째, 공격자가 임의의(malformed) locale 식별자를 FreeMarker에 전달할 수 있어야 합니다. 이 두 번째 조건이 얼마나 쉽게 성립하는지는 이 라이브러리를 쓰는 애플리케이션마다 다릅니다 — 예를 들어 요청 헤더나 파라미터로 받은 사용자 언어 설정을 그대로 FreeMarker에 넘기는 구조라면, 사실상 원격에서 공격자가 이 값을 주무를 수 있습니다. ⚠️미확인 — FreeMarker 자체 어드바이저리는 이 경로가 무인증(pre-auth)인지 여부를 일반적으로 명시하지 않으며, 이는 각 소비 애플리케이션의 구현에 달려 있습니다.

  • 영향 범위: 2.2.0 ~ 2.3.34 (해당 범위 전체 포함).
  • 수정 버전: 2.3.35.
  • CVSS: 3.1 기준 9.1 (Critical), 벡터 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N.
  • CWE: CWE-23 (Relative Path Traversal) — 다수 출처(집계 사이트·GHSA 스니펫)가 일치합니다. 단 일부 출처는 CWE-22(일반 Path Traversal)로 표기합니다. ⚠️미확인 — 어느 쪽이 NVD 정본인지 팩트체크 대상입니다.
  • GHSA: GHSA-27j2-h3m2-8237로 알려져 있습니다. ⚠️미확인 — 원본 GitHub Advisory 페이지를 직접 확인하지 못했고, 2차 인용으로만 확보된 값입니다.
  • 패치 커밋: a89250840e9016d76a2fce98fda18a1ddb51db24(작성 2026-08-22, 커밋 2026-08-23). ⚠️미확인 — 라인 단위 대조는 아래 §6에서 별도 표시합니다.

한 줄 요약: “언어 이름을 파일 경로에 그대로 이어붙이면, 언어 이름이 아니라 경로 탈출 문자열을 보낼 수 있다” — 지역화 조회는 편의 기능이지만, 그 입력값이 경로 조립의 재료로 쓰이는 순간 신뢰할 수 없는 입력이 됩니다.

기술 배경 — “지역화 템플릿 조회”란 무엇인가

FreeMarker는 같은 템플릿을 여러 언어로 서빙해야 하는 상황을 돕기 위해, 템플릿 이름 하나로 여러 언어 변형을 자동으로 찾아주는 기능을 제공합니다. 예를 들어 애플리케이션이 home.ftl 템플릿을 요청하면서 사용자의 locale이 ko_KR이라면, FreeMarker는 다음 순서로 파일을 찾습니다.

  1. home_ko_KR.ftl
  2. home_ko.ftl
  3. home.ftl (언어별 버전이 없으면 기본값)

이 조회 과정에서 FreeMarker는 요청받은 파일 이름의 확장자 앞에 _ + locale 문자열을 끼워 넣어 후보 경로들을 만들고, 각 후보를 실제 TemplateLoader(파일시스템·클래스패스·서블릿 컨텍스트 등에서 템플릿을 읽어오는 구현체)에 넘겨 존재 여부를 확인합니다. 여기서 핵심은 이 조합 과정에 쓰이는 locale 문자열 자체는 애초에 “언어 이름”이라는 가정 하에 별다른 검증을 받지 않았다는 점입니다.

CVE 분석 요약 — 어느 판단이 원인인가

리서치 재료 기준, 근본 원인은 두 층에 걸쳐 있습니다.

① locale 값이 검증 없이 경로 조립에 쓰입니다. FreeMarker의 지역화 조회 로직(freemarker.cache 패키지, TemplateCache 관련 코드로 알려져 있습니다 — ⚠️미확인, 정확한 클래스·메서드명과 v2.3.35에서 이 부분 자체가 수정됐는지는 팩트체크 대상)은 Locale.toString() 값을 템플릿 이름 문자열에 그대로 이어붙여 조회용 경로를 만듭니다. locale 값 안에 ..처럼 경로를 거슬러 올라가는 시퀀스가 들어 있어도, 이 조합 단계에서는 걸러지지 않습니다.

② 일부 TemplateLoader 구현체는 최종 경로를 자체 검증하지 않았습니다. FreeMarker는 실제로 파일을 읽어오는 방식이 여러 가지입니다(파일시스템에서 읽는 FileTemplateLoader, 자바 클래스패스에서 읽는 ClassTemplateLoader, 웹 애플리케이션 컨텍스트에서 읽는 WebappTemplateLoader 등). 이 중 FileTemplateLoader는 원래부터 베이스 디렉터리 밖으로의 이탈을 자체적으로 차단하고 있었던 것으로 보입니다. 반면 ClassTemplateLoader와 WebappTemplateLoader는 TemplateCache가 넘겨주는 경로가 이미 안전하다고 가정하고, 자체적인 이탈 검증 로직이 없었습니다. 결국 ①에서 만들어진, ..가 섞인 경로가 ②의 검증 없는 로더에 그대로 전달되면, 베이스 디렉터리 바깥의 파일을 읽어올 수 있는 조건이 만들어집니다.

아래는 공개된 패치 커밋(a892508)에서 확인된 방어 코드를 개념적으로 재구성한 것입니다. 정확한 원본 라인번호는 ⚠️미확인(팩트체크 시 raw 소스 재대조 필요)이며, 아래 스니펫은 패치가 어떤 형태로 방어를 추가했는지 보여주기 위한 단순화입니다.

// 개념용 단순화, 실제 패치 코드 원문 아님 (라인번호 미확인 — 팩트체크 대상)
// 패치 후: ClassTemplateLoader.getURL() 계열에 베이스 디렉터리 이탈 검사 추가
protected URL getURL(String name) {
    // (신규) 요청받은 이름이 베이스 디렉터리를 벗어나는지 먼저 검사한다
    if (!isInsideBaseDir(name)) {
        throw new MalformedTemplateNameException(name, "backing out from the base directory is not allowed");
    }
    String fullPath = basePackagePath + name;   // 패치 전에는 이 조합 결과를 그대로 신뢰했다
    return classLoader.getResource(fullPath);
}

정리하면, 패치는 locale 문자열의 조합 지점을 고치는 대신(혹은 그에 더해), 최종 소비 지점인 로더 레벨에 이탈 검증을 추가하는 방향으로 이뤄진 것으로 보입니다. 이는 신뢰할 수 없는 입력이 여러 컴포넌트를 거쳐 흐를 때, “입구에서 한 번 막기”보다 “값을 실제로 소비하는 모든 지점에서 검증”하는 것이 더 견고하다는 일반적인 교훈과 일치합니다. 다만 TemplateCache 쪽 조합 로직 자체가 이번 패치로 추가 수정됐는지는 이 글의 리서치 범위에서 diff까지 확인하지 못했습니다(⚠️미확인).

공격 시나리오 · 영향도 + 대응방안

공격 시나리오 (단계 압축, 완성형 페이로드 없음):

  1. 애플리케이션이 사용자 요청(파라미터·헤더 등)으로부터 얻은 값을 검증 없이 FreeMarker의 locale로 사용하는 경로가 있어야 합니다.
  2. 공격자가 정상적인 언어 코드(ko_KR 등) 대신, 경로 이탈 시퀀스(..)가 섞인 malformed locale 문자열을 전달합니다.
  3. “지역화 조회”가 기본 활성 상태이므로, FreeMarker는 이 값을 그대로 후보 경로 조합에 사용하고, 방어 로직이 없는 TemplateLoader 구현체(ClassTemplateLoader/WebappTemplateLoader 등)를 통해 베이스 디렉터리 바깥의 파일을 읽어들일 수 있습니다.

영향도: CVSS 벡터상 기밀성(C)·무결성(I) High, 가용성(A) None입니다. 기밀성 영향은 베이스 디렉터리 밖의 파일을 읽어 그 내용을 응답에 노출시킬 수 있다는 뜻으로 이해됩니다. 무결성 영향까지 High로 표기된 이유는 이 글의 리서치 범위에서 명확한 근거를 확보하지 못했습니다 — ⚠️미확인(추정): 읽어들인 임의 파일이 다시 FreeMarker 템플릿으로 해석·렌더링되는 구조라면, 공격자가 애플리케이션 응답의 내용 자체를 조작하는 효과(템플릿 삽입에 준하는 결과)로 이어질 수 있다는 것이 우리의 추론이며, 이는 FreeMarker 공식 자료로 확증된 사실이 아닙니다. 실제로 어떤 파일까지 읽을 수 있는지는 애플리케이션이 어떤 TemplateLoader를 쓰는지에 달려 있습니다 — FileTemplateLoader만 쓰는 구성은 원래부터 자체 검증이 있어 영향이 제한적이었을 가능성이 있고, ClassTemplateLoader/WebappTemplateLoader를 쓰는 구성이 이번 패치의 핵심 대상이었던 것으로 보입니다(⚠️미확인).

대응방안:

  1. 근본: FreeMarker를 2.3.35 이상으로 업데이트합니다. 이 라이브러리를 의존성으로 포함하는 모든 애플리케이션(직접 의존이든 전이 의존이든)을 함께 점검해야 합니다.
  2. 차선(즉시 업데이트 불가 시):
    • 애플리케이션 설정에서 지역화 조회(localized lookup)를 비활성화합니다. FreeMarker Configuration 객체의 관련 설정값을 꺼서, locale 기반 경로 조합 자체가 일어나지 않게 만드는 방식입니다.
    • 사용자 입력(요청 헤더·파라미터)을 FreeMarker에 전달되는 locale 값으로 직접 쓰지 않도록, 애플리케이션이 허용된 locale 목록(화이트리스트)으로만 매핑해 전달합니다.
    • TemplateLoader 구현을 직접 커스터마이즈해 사용 중이라면, 자체적으로 베이스 디렉터리 이탈 여부를 검증하는 로직이 있는지 점검합니다.
  3. 탐지 관점: 애플리케이션 로그에서 locale/언어 관련 파라미터·헤더 값에 ../나 인코딩된 변형(%2e%2e%2f 등)이 포함된 요청을 감시합니다. 평소 locale 값의 형식(언어_국가 패턴)에서 크게 벗어난 값은 그 자체로 이상 신호입니다.

결론

지역화 조회는 “사용자에게 맞는 답을 찾아주는” 편의 기능입니다. 그런데 그 답을 찾는 방식이 “사용자가 말한 언어 이름을 파일 경로에 그대로 끼워 넣는” 것이었다면, 편의는 곧 입력 하나를 검증 없이 파일시스템까지 흘려보내는 통로가 됩니다.

이 부류의 사고가 반복되는 이유는 분명합니다. 경로를 구성하는 데 쓰이는 어떤 값이든, 그 값의 출처가 사용자가 통제 가능한 입력이라면 경로 조작의 재료가 될 수 있습니다. 문제는 “이건 그냥 언어 코드니까 안전할 것”이라는, 값의 용도만 보고 신뢰성을 판단하는 습관입니다. 이번 패치가 파일 이름 자체보다 한 단계 뒤에 있는 locale 문자열의 조립 지점을 노렸다는 점도 흥미롭습니다 — 흔히 파일 업로드나 다운로드 파라미터만 경로 탈출의 입구로 경계하지만, “부가 정보”처럼 보이는 값도 결국 문자열 조합의 재료가 되면 같은 위험을 갖습니다. 이 결함은 우리가 다룬 다른 경로 탐색(Path Traversal, CWE-22/23) 계열, 그리고 신뢰할 수 없는 입력을 여러 컴포넌트가 거쳐가며 “누군가 이미 검증했겠지”라고 가정하는 경계 책임 분산 문제와 같은 뿌리를 공유합니다. 정석은 값이 소비되는 모든 지점(이번 경우 TemplateLoader 구현체 각각)에서 독립적으로 검증하는 것입니다. 이 한 틈은 2.3.35에서 닫혔지만, “부가 값도 경로 조합에 쓰이면 위험한 입력이 된다”는 교훈은 다음 라이브러리, 다음 조회 로직에서 또 시험대에 오릅니다. 그래서 우리는 도구가 아니라 방법을 남깁니다.

References / 참고자료

  1. Apache Software Foundation, oss-security 메일링리스트 공지(Openwall 재게시), “CVE-2026-84939: Apache FreeMarker Path Traversal” — 이 글이 재구성한 1차 정본(취약 버전·수정 버전·선행조건). https://www.openwall.com/lists/oss-security/2026/09/08/2
  2. Apache FreeMarker, GitHub 패치 커밋 — 실제 패치 코드(ClassTemplateLoader/WebappTemplateLoader 베이스 디렉터리 이탈 검증 추가). https://github.com/apache/freemarker/commit/a89250840e9016d76a2fce98fda18a1ddb51db24
  3. Strix.ai, “CVE-2026-84939: Freemarker Vulnerability (CVSS 9.1)” — CVSS 벡터·CWE 분류 교차 확인. https://www.strix.ai/cve/CVE-2026-84939
  4. VulDB, “CVE-2026-84939 in FreeMarker” — 영향 버전·CVSS 교차 확인. https://vuldb.com/cve/CVE-2026-84939
  5. OpenCVE, “CVE-2026-84939 - Vulnerability Details” — CVSS 교차 확인. https://app.opencve.io/cve/CVE-2026-84939
  6. NVD, “CVE-2026-84939” — 정본 레코드(팩트체크 시 직접 대조 필요, 리서치 단계에서는 간접 확인만 됨). https://nvd.nist.gov/vuln/detail/CVE-2026-84939

이 글은 위 원 자료를 우리 관점에서 다시 쓰고 논평을 더한 것입니다. 특정 문장·코드를 그대로 옮기지 않았으며, 자세한 재현 절차는 원문을 참고하세요. 이 초안은 fetch 없이 리서치 재료만으로 작성됐습니다. ⚠️미확인 표시가 붙은 사실 주장(GHSA 번호·CWE 정본 분류·선행조건의 인증 요구·리포터·TemplateCache 변경 여부·패치 코드의 정확한 라인번호·무결성 영향의 원인)은 발행 전 독립 팩트체크로 1차출처 대조가 끝나야 확정됩니다.