| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2026-62390 |
| 영향 제품/버전 | Apache Kylin 4.0.0 이상 5.0.4 미만 |
| CVSS v3.1 | 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-89 (SQL 명령의 특수 요소 부적절한 중화) |
| 공개일 | 2026-07-14 |
| 수정 버전 | 5.0.4 |
| 수정 커밋 | f274934caf1d071ccd226043f064a6e9016730d7 (KYLIN-6089) |
| 취약 지점 | TableService.refreshTable()의 REFRESH TABLE SQL 문자열 조립 |
Apache Kylin은 대규모 데이터 분석을 위한 OLAP 엔진이며, 백엔드의 카탈로그 캐시 새로고침 흐름은 테이블 이름을 받아 Spark SQL의 REFRESH TABLE 문을 실행한다. 5.0.3 이하의 TableService.refreshTable()은 받은 문자열을 SQL 구문 뒤에 그대로 이어 붙였다. 따라서 입력이 단순한 테이블 식별자라는 전제가 깨지면, 식별자 자리에 SQL 구문 일부가 섞여 생성 SQL에 주입될 수 있었다.
5.0.4는 빈 값, 세 개 이상으로 나뉜 이름, 특수 문자를 거부하고 각 조각을 [\\w_]+로 제한한다. 즉 "테이블 이름"이라는 좁은 문법만 SQL 생성 경계 너머로 보낸다. Apache/NVD는 영향 범위를 4.0.0 이상 5.0.4 미만으로, 해결책을 5.0.4 업그레이드로 명시한다.
Kylin은 SQL 인터페이스를 제공하는 분산 OLAP 엔진이다. 분석 서버는 메타데이터와 테이블 정보를 계속 다시 읽기보다 카탈로그 캐시를 사용한다. 원본 테이블의 스키마나 메타데이터가 바뀐 뒤 캐시를 비우고 다시 읽어야 할 때, 실행 엔진에 REFRESH TABLE <식별자>를 요청할 수 있다. 여기서 <식별자>는 보통 sales 또는 warehouse.sales처럼 테이블을 가리키는 데이터 값이 아닌 문법 조각이다.
정상 흐름에서 API는 새로고칠 테이블 목록을 받고, 서비스는 각 이름마다 고정된 명령의 빈칸만 채운다. 실행 엔진은 그 명령을 파싱해 카탈로그를 갱신한다.
요청의 tables 목록
│
▼
TableService.refreshSingleCatalogCache()
각 table을 refreshTable()에 전달
│
▼
"REFRESH TABLE " + table
│
▼
PushDownUtil.trySimplyExecute() → Spark SQL 파서/실행 엔진
│
▼
해당 테이블의 카탈로그 캐시 갱신
준비된 문(statement)의 바인드 변수는 보통 문자열·숫자 같은 값을 SQL 문법과 분리한다. 그러나 REFRESH TABLE ?의 ? 자리에 테이블 이름을 넣는 방식은 일반적인 SQL 바인딩으로 해결되지 않는 경우가 많다. 테이블명은 파서가 문법으로 읽는 식별자이기 때문이다. 이때 안전한 설계는 문자열 연결 자체를 믿는 것이 아니라, 연결 전에 입력이 허용된 식별자 문법에 정확히 속하는지 검사하는 것이다.
이 흐름이 요구하는 불변식은 다음과 같다.
실행 엔진으로 전달되는
REFRESH TABLE명령의 가변 부분은 하나 또는db.table형태의 테이블 식별자여야 하며, SQL 구분자·공백·따옴표·추가 키워드를 포함해서는 안 된다.
예를 들어 db.orders는 두 조각의 식별자이므로 정상 범주지만, 구분자나 여분의 구문이 포함된 문자열은 테이블명이 아니라 SQL 문장의 구조를 바꾸려는 입력이다. 이 차이를 검사하지 않으면 문자열 연결의 경계는 SQL 파서의 경계와 같아진다.
의도한 입력 언어 SQL 파서가 받는 언어
----------------------------- ---------------------------------
table | db.table REFRESH TABLE <임의 SQL 토큰열>
↑ ↑
└─ 애플리케이션이 보장해야 할 └─ 검증 없이 연결하면 이 범위까지
좁은 허용 집합 입력이 확장됨
취약 함수는 예외를 잡아 해당 테이블을 failed 목록에 기록한다. 이는 호출자에게 실패를 보고하는 기능이다. 하지만 안전 경계는 예외 처리 뒤가 아니라 SQL 실행 호출 전에 있어야 한다. 실행 엔진이 입력을 파싱하고 일부 동작을 수행한 뒤 실패할 수도 있으므로, "결과가 실패였다"는 사실만으로 주입이 실행되지 않았음을 보장하지 않는다.
5.0.3의 관련 흐름은 요청의 tables 목록 원소를 별도 검증 없이 refreshTable()로 넘기고, 그 값을 고정 접두사에 연결했다.
public TableRefresh refreshSingleCatalogCache(Map refreshRequest) {
List<String> tables = (List<String>) refreshRequest.get("tables");
tables.forEach(table -> refreshTable(table, refreshed, failed));
}
public void refreshTable(String table, List<String> refreshed, List<String> failed) {
try {
PushDownUtil.trySimplyExecute("REFRESH TABLE " + table, null);
refreshed.add(table);
} catch (Exception e) {
failed.add(table);
}
}문제는 table이 객체 모델의 TableIdentifier가 아니라 일반 String이라는 점이다. 함수는 SQL 키워드는 고정했지만 가변 부분에 공백, 구분자, 인용, 추가 구문이 없는지를 확인하지 않는다. 그 결과 실행 엔진이 받아들이는 최종 문장은 API가 의도한 "테이블 새로고침"보다 넓은 SQL 언어가 될 수 있다.
[패치 전]
HTTP 요청의 tables[] 원소: 공격자가 제어한 문자열
│
▼
refreshTable(table)
│
▼
"REFRESH TABLE " + table
│
├─ 정상 식별자 → 의도한 캐시 갱신
│
└─ SQL 구조 문자가 섞인 문자열
│
▼
Spark SQL 파서가 생성된 전체 문장을 해석
│
▼
테이블명이라는 불변식 상실 → SQL 주입 가능성
공식 CVE 설명도 "테이블 카탈로그를 새로고침하는 백엔드 API"가 생성 SQL에 주입을 일으킬 수 있다고 한정한다. 공개 자료와 수정 diff는 입력 검증 누락을 확정하지만, 실제 배포에서 이 API가 어떤 인증·권한 정책으로 노출되는지는 배포 설정에 따라 확인해야 한다. NVD의 PR:N 벡터는 영향 평가에 포함되어 있으나, 이 보고서는 해당 벡터만으로 개별 설치의 인증 우회를 주장하지 않는다.
수정 커밋은 SQL 실행 전 isValidTableIdentifier()를 추가했다. 빈 문자열은 거부하고, 점으로 나눈 조각 수를 최대 둘로 제한한 뒤, 각 조각이 [\\w_]+ 전체 일치인지 검사한다. 따라서 허용되는 것은 table 또는 database.table의 단순 식별자이며, 공백·세미콜론·백틱·빈 조각·세 조각 이상의 이름은 실행 함수에 도달하지 못한다.
public void refreshTable(String table, List<String> refreshed, List<String> failed) {
try {
+ if (!isValidTableIdentifier(table)) {
+ throw new KylinException(INVALID_NAME, ...);
+ }
PushDownUtil.trySimplyExecute("REFRESH TABLE " + table, null);
} catch (Exception e) {
failed.add(table);
}
}
+private static boolean isValidTableIdentifier(String table) {
+ if (StringUtils.isBlank(table)) return false;
+ String[] parts = table.split("\\.", -1);
+ if (parts.length > 2) return false;
+ for (String part : parts)
+ if (!Pattern.compile("([\\w_]+)").matcher(part).matches()) return false;
+ return true;
+}[패치 후]
tables[] 원소
│
▼
isValidTableIdentifier()
│
├─ blank / 특수문자 / 빈 조각 / 3개 이상 조각
│ └─ INVALID_NAME → failed 목록 기록 → SQL 미실행
│
└─ table 또는 db.table, 모든 조각이 [\w_]+
▼
"REFRESH TABLE " + 검증된 식별자
▼
PushDownUtil.trySimplyExecute()
커밋의 회귀 테스트는 빈 값, 공백, 하이픈, 공백 포함 이름, 세미콜론 뒤 SQL 텍스트, 백틱, db.tbl.extra, 빈 조각 등을 모두 실패 목록에 넣는지 확인한다. 운영자는 Apache Kylin을 5.0.4 이상으로 업그레이드해야 한다. 업그레이드 전 임시 완화가 필요하면 이 카탈로그 캐시 새로고침 API의 네트워크 노출을 제한하고, 신뢰되지 않은 사용자가 tables 목록을 제어하지 못하게 해야 한다. 다만 공식 권고는 임시 설정 변경이 아니라 업그레이드이므로, 이 조치는 패치를 대체하지 않는다.
- 공개 CVE와 upstream 수정은 취약 함수·영향 버전·식별자 검증 방식을 명확히 한다. 반면 공개 권고에는 공격 요청의 정확한 엔드포인트와 각 배포 구성에서의 인증 경로가 상세히 기술되지 않았다. 이 보고서는 이를 추정하거나 재현하지 않았다.
[\\w_]+허용 규칙은 SparkSessionCatalog#validateName을 참조한 upstream 선택이다. 기존에 인용 식별자나 특수 문자가 든 테이블명을 사용하던 배포에서는 5.0.4 적용 전 호환성 시험이 필요하다.
- CVE-2026-62390 — NVD — 영향 범위, CVSS, CWE, 5.0.4 업그레이드 권고
- Apache Kylin 보안 권고 — Apache CNA 원문 권고
- 수정 커밋
f274934— 식별자 검증과 음성 회귀 테스트 - Apache Kylin 개요 — Kylin의 OLAP/SQL 인터페이스와 5.0 배경