| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2026-62392 |
| 영향 제품/버전 | Apache Kylin 4.0.0 이상 5.0.4 미만 |
| CVSS v3.1 | 9.8 (Critical, NVD) / 8.8 (Secondary, AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) |
| CWE | CWE-78 (OS 명령의 특수 요소 부적절한 중화) |
| 공개일 | 2026-07-14 |
| 수정 버전 | 5.0.4 |
| 수정 커밋 | 5a3cf7e60ee37d376799dcf5db0315bd6fa4241b (KYLIN-6091, "[MINOR] Fix job handler") |
| 취약 지점 | DefaultSparkBuildJobHandler.appendSparkConf()의 spark-submit 셸 명령 문자열 조립 |
Apache Kylin은 빅데이터 OLAP 엔진으로, 인덱스가 없는 쿼리를 소스 엔진(Spark)에 그대로 넘기거나(pushdown) 비동기 쿼리 결과를 파일로 뽑아낼 때 spark-submit 하위 프로세스를 띄운다. 이 하위 프로세스를 실행하는 명령줄은 Java 코드가 문자열을 이어 붙여 만든 뒤 /bin/bash -c <문자열>로 그대로 실행된다.
비동기 쿼리 API는 클라이언트가 실행에 쓸 YARN 큐 이름(spark_queue)을 요청 본문에 직접 지정할 수 있게 해 둔다. 이 값은 검증 없이 --conf 'spark.yarn.queue=<값>' 형태로 명령줄에 삽입된다. 값 안에 작은따옴표(') 하나만 넣으면 셸의 인용 구간이 그 자리에서 끊어지고, 그 뒤 문자열은 셸이 명령 구분자(;, && 등)로 해석하는 일반 텍스트가 된다. 5.0.3 이하는 이 값에 대해 오직 "역따옴표(`)나 $(...) 포함 여부"만 검사했을 뿐, 작은따옴표 자체는 걸러내지 않았다. 결과적으로 인증된 사용자(해당 프로젝트에 대한 쿼리 권한 보유자)가 Kylin 서버 프로세스 권한으로 임의 OS 명령을 실행할 수 있었다. 5.0.4는 --conf에 들어가는 키·값에 작은따옴표가 섞여 있으면 명령줄을 만들기 전에 예외를 던지도록 고쳤다.
Kylin 자체는 SQL을 받는 쿼리 서버지만, 실제 대규모 인덱스 빌드나 인덱스가 커버하지 못하는 쿼리의 실행은 Spark 클러스터에 위임한다. Kylin 서버 프로세스는 이때 spark-submit이라는 셸 스크립트를 별도 OS 프로세스로 실행해 Spark 애플리케이션을 제출한다. Java 프로그램이 외부 실행 파일을 띄우는 표준 방법은 ProcessBuilder이며, 이 클래스는 두 가지 아주 다른 쓰임새를 지원한다.
new ProcessBuilder("spark-submit", "--class", "Foo", ...)처럼 인자 배열을 넘기면, OS는 각 배열 원소를 셸 해석 없이 그대로 실행 파일의argv로 전달한다. 이 경로에는 셸이 끼어들 자리가 없으므로 인자 값에;나`가 들어 있어도 그냥 문자 그대로 취급된다.new ProcessBuilder("/bin/bash", "-c", command)처럼 완성된 명령 문자열 하나를 넘기면, 그 문자열 전체가/bin/bash에 의해 다시 파싱된다. 이 경로에서는command문자열 안의 모든 셸 메타문자(공백, 따옴표,;,&&,`,$(), 파이프 등)가 살아서 해석된다.
두 번째 경로를 쓰면서 command 문자열의 일부를 신뢰할 수 없는 외부 입력으로 채우는 것이 OS 커맨드 인젝션의 전형적인 발생 패턴이다. Kylin의 spark-submit 실행 경로는 export HADOOP_CONF_DIR=... && .../bin/spark-submit --class ... --conf '...' ... <jar> <args>라는 하나의 긴 문자열을 만들어 이 두 번째 경로로 넘긴다 — 즉 셸이 최종적으로 해석하는 대상이다.
POSIX 셸에서 작은따옴표로 감싼 구간('...')은 내부의 모든 문자를 글자 그대로 취급한다 — 공백, $, `, ; 무엇이든 특별한 의미를 잃는다. 이 성질 덕분에 Kylin은 --conf 값에 공백이 섞여도(spark.driver.extraJavaOptions=-Dx=1 -Dy=2 등) 한 덩어리로 넘기려고 값을 작은따옴표로 감싼다.
문제는 작은따옴표 인용 구간 안에서는 작은따옴표 자기 자신만은 이스케이프할 방법이 없다는 점이다(백슬래시조차 작은따옴표 안에서는 특별하지 않다). 그래서 애플리케이션이 값 자체에 작은따옴표가 들어 있는지 미리 검사하지 않고 그대로 '...'로 감싸면, 값에 포함된 ' 문자가 곧바로 인용 구간을 닫아 버린다. 이후의 텍스트는 다시 "따옴표 밖" 셸 문법으로 해석되고, 거기서 ;나 &&를 쓰면 완전히 새로운 명령을 이어붙일 수 있다. 이것이 이번 취약점의 유일한 트리거다 — 역따옴표나 $() 같은 명령 치환 문법조차 필요 없다.
Kylin은 대용량 쿼리 결과를 CSV/파케이 등 파일로 비동기로 뽑아내는 "비동기 쿼리" API를 제공한다. 멀티테넌시 환경에서 특정 프로젝트가 다른 프로젝트의 쿼리 자원을 잠식하지 않도록, 프로젝트 설정으로 "비동기 쿼리마다 전용 YARN 큐를 쓴다"(isUniqueAsyncQueryYarnQueue)는 옵션을 켤 수 있다. 이 옵션이 켜진 프로젝트에서는 요청자가 지정한 큐 이름이 우선순위를 갖고(request param > project config > system config) 실제 spark-submit 명령의 --conf spark.yarn.queue=<값>으로 그대로 흘러간다.
요청 스키마에 사용자가 직접 채우는 문자열 필드가 있고, 별도 형식 검증이 없다.
// common-service/.../request/SQLRequest.java
@JsonProperty("spark_queue")
private String sparkQueue = "";이 값은 isUniqueAsyncQueryYarnQueue 프로젝트에서 그대로 사용된다.
// query-service/.../service/QueryService.java
if (StringUtils.isNotEmpty(sqlRequest.getSparkQueue())) {
queryParams.setSparkQueue(sqlRequest.getSparkQueue());
}작업(job) 파라미터로 넘어간 뒤, Spark 설정 오버라이드 맵의 spark.yarn.queue 키 값이 된다.
// query-service/.../service/AsyncQueryJob.java
protected Map<String, String> getSparkConfigOverride(KylinConfig config) {
Map<String, String> overrides = config.getAsyncQuerySparkConfigOverride();
// async query spark queue priority: request param > project config > system config
overrides.put("spark.yarn.queue", getParam(NBatchConstants.P_QUERY_QUEUE));
...
}DefaultSparkBuildJobHandler는 이 맵을 순회하며 명령 문자열에 이어 붙인다. 값에 대한 검증은 없다.
// engine-spark/.../job/DefaultSparkBuildJobHandler.java (5.0.3)
protected void appendSparkConf(StringBuilder sb, String confKey, String confValue) {
// Multiple parameters in "--conf" need to be enclosed in single quotes
sb.append(" --conf '").append(confKey).append(EQUALS).append(confValue).append("' ");
sb.append(SUBMIT_LINE_FORMAT);
}명령 문자열이 완성되면 checkCommandInjection()이 한 번 더 검사하지만, 정규식이 잡는 패턴은 역따옴표와 $(...)뿐이다.
private void checkCommandInjection(String command) {
...
Matcher matcher = Pattern.compile("(`[^`]*+`)|(\\$\\([^)]*+)").matcher(command);
while (matcher.find()) illegals.add(matcher.group());
if (!illegals.isEmpty()) throw new IllegalArgumentException(...);
}그리고 최종적으로 /bin/bash -c로 실행된다.
// core-common/.../util/CliCommandExecutor.java
String[] cmd = new String[3];
cmd[0] = "/bin/bash";
cmd[1] = "-c";
cmd[2] = command;
ProcessBuilder builder = new ProcessBuilder(cmd);
Process proc = builder.start();[패치 전]
POST /kylin/api/async_query
body: { "sql": "...", "spark_queue": "default'; touch /tmp/pwned; echo '" }
│ (isUniqueAsyncQueryYarnQueue 프로젝트, 쿼리 권한만 있으면 됨)
▼
QueryService: queryParams.setSparkQueue(sqlRequest.getSparkQueue())
│
▼
AsyncQueryJob.getSparkConfigOverride()
overrides.put("spark.yarn.queue", "default'; touch /tmp/pwned; echo '")
│
▼
appendSparkConf(sb, "spark.yarn.queue", value)
sb.append(" --conf '").append(key).append("=").append(value).append("' ")
│
▼
...--conf 'spark.yarn.queue=default'; touch /tmp/pwned; echo '' ...
└┬┘└─────────┬────────┘└┬┘
따옴표 종료 셸이 그대로 실행 새 따옴표로
(의도한 값 끝) 하는 두번째 명령 다시 짝맞춤
▼
checkCommandInjection(command)
정규식: (`[^`]*`)|($(...)) ← ';'는 이 패턴에 걸리지 않음 → 통과
▼
CliCommandExecutor: new ProcessBuilder("/bin/bash", "-c", command)
▼
touch /tmp/pwned 가 Kylin 서버 프로세스 권한으로 실행됨
checkCommandInjection은 명령 치환(`, $())만 막도록 설계되어 있었다. 그러나 작은따옴표 탈출은 명령 치환이 아니라 셸 인용 구간 자체를 끊는 것이므로 이 정규식의 탐지 대상 밖에 있었다. 두 검사(작은따옴표 미검증 + 명령 치환만 보는 필터)가 겹쳐서, 세미콜론 하나로 충분한 인젝션이 끝까지 살아남았다.
수정 커밋은 appendSparkConf()에서 문자열을 이어 붙이기 전에 키와 값에 작은따옴표가 포함돼 있는지 검사해, 있으면 명령줄을 만들지 않고 바로 예외를 던진다.
protected void appendSparkConf(StringBuilder sb, String confKey, String confValue) {
// Multiple parameters in "--conf" need to be enclosed in single quotes
+ if (confKey.contains("'") || confValue.contains("'")) {
+ throw new IllegalArgumentException(String.format(Locale.ROOT,
+ "Spark conf key or value contains invalid single quote: key=%s, value=%s",
+ confKey, confValue));
+ }
sb.append(" --conf '").append(confKey).append(EQUALS).append(confValue).append("' ");
sb.append(SUBMIT_LINE_FORMAT);
}[패치 후]
spark.yarn.queue = "default'; touch /tmp/pwned; echo '"
│
▼
appendSparkConf(sb, key, value)
│
├─ key 또는 value에 ' 포함?
│ └─ YES → IllegalArgumentException
│ → 명령 문자열 생성 자체가 중단됨
│ → spark-submit 미실행
│
└─ NO → 기존과 동일하게 --conf 'key=value' 로 결합
▼
checkCommandInjection() (기존 방어선, 유지)
▼
/bin/bash -c 로 실행
checkCommandInjection의 역따옴표/$() 필터는 그대로 남아 있다 — 이번 패치는 그 필터가 놓치던 "작은따옴표로 인용 구간을 끊는" 경로 하나를 앞단에서 추가로 차단한 것이다. 회귀 테스트는 appendSparkConf에 작은따옴표가 든 값(default'; touch /tmp/pwned; echo ')을 넣었을 때 예외가 나는지, 그리고 checkCommandInjection이 역따옴표·$() 페이로드를 여전히 막는지를 각각 확인한다. 운영자는 Apache Kylin을 5.0.4 이상으로 업그레이드해야 한다. 업그레이드 전 완화책으로는 isUniqueAsyncQueryYarnQueue를 켠 프로젝트를 최소화하거나, 신뢰되지 않은 사용자에게 비동기 쿼리 API 접근을 주지 않는 방법이 있지만, 이는 공식 수정을 대체하지 않는다.
- 공식 권고문(oss-security 메일)은 "백엔드 API가 job config 파라미터를 OS 명령줄로 가져간다"고만 서술하고 구체적 필드명을 밝히지 않는다. 이 보고서가 지목한
spark_queue→spark.yarn.queue경로는 소스 코드 추적으로 직접 확인한 것이며, JIRA 제목("Job handler parameters validation")·수정 커밋 위치(DefaultSparkBuildJobHandler)·CVE 부제("OS Command Injection via Async Query API")와 모두 일치하지만, Apache 측이 이 경로를 유일한 공격 벡터로 명시적으로 확인해 준 것은 아니다. appendSparkConf는wrapSparkConf를 통해sparkConf맵의 모든 항목에 적용되므로, 이번 패치는spark.yarn.queue하나만이 아니라 셸로 전달되는 모든 Spark 설정 오버라이드 항목에 동일하게 적용된다. 다만 이 보고서는spark_queue외에 사용자가 직접 채울 수 있는 다른 오버라이드 경로가 실제로 존재하는지까지는 확인하지 않았다.- NVD가 표기한 두 CVSS 벡터(
PR:N9.8 vsPR:L8.8) 중 어느 쪽이 실제 배포 조건을 반영하는지는 공식 문서에 설명이 없다.async_query엔드포인트는aclEvaluate.checkProjectQueryPermission()으로 프로젝트 쿼리 권한을 요구하므로, 최소한 익명 접근은 아니라는 점은 코드로 확인했다.
- CVE-2026-62392 — NVD — 영향 범위, CVSS, CWE, 5.0.4 업그레이드 권고
- Apache Kylin 보안 권고 (oss-security 메일링 리스트) — "OS Command Injection via Async Query API", KYLIN-6091 트래킹
- 수정 커밋
5a3cf7e—appendSparkConf작은따옴표 검증 추가와 회귀 테스트 DefaultSparkBuildJobHandler.java(5.0.3 태그) — 패치 전 명령 조립·검사 로직 전체 확인CliCommandExecutor.java(5.0.3 태그) —/bin/bash -c실행 경로 확인