| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2026-50559 |
| 영향 소프트웨어 | io.quarkus:quarkus-vertx-http — 3.20.6.2 이전, 3.21.0 이상 3.27.4.1 이전, 3.28.0 이상 3.33.2.1 이전, 3.34.0 이상 3.36.3 이전 |
| 취약 지점 | AbstractPathMatchingHttpSecurityPolicy와 후단 라우터·정적 파일 핸들러 사이의 경로 해석 불일치 |
| CWE | CWE-551 (Incorrect Behavior Order: Authorization Before Parsing and Canonicalization) |
| CVSS v3.1 | CVSS 7.5 High — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| 공개일 | 2026-06-17 |
| 수정 버전 | 3.20.6.2, 3.27.4.1, 3.27.5, 3.33.2.1, 3.33.3, 3.36.3, 3.37.0 |
| 분석 기준 소스 | Quarkus 3.36.2(취약)와 3.37.0(수정) 공개 태그 |
Quarkus 애플리케이션은 quarkus.http.auth.permission 설정으로 특정 HTTP 경로에 인증이나
역할 정책을 붙일 수 있다. 이 보안 계층은 요청 경로를 Vert.x의 normalizedPath()로 읽고
정책 패턴과 비교한다. 그런데 normalizedPath()는 RFC 3986의 비예약 문자만 디코딩하므로,
세미콜론·슬래시·역슬래시 같은 예약 문자는 %3B, %2F, %5C 형태로 남는다.
취약 버전은 이 부분 디코딩 결과에서 리터럴 ;만 찾아 matrix parameter를 제거했다.
따라서 인코딩된 세미콜론은 보안 계층을 그대로 통과해 보호 경로와 다른 문자열로 비교될 수
있었다. 정적 파일 핸들러는 이후 경로를 완전히 percent-decode하고 역슬래시를 슬래시로
바꾸므로, 보안 계층이 보호 대상이 아니라고 본 %2F·%5C 경로가 실제 파일을 찾을 때는
보호 경로와 같아질 수도 있었다.
깨진 불변식은 **“권한을 결정할 때 사용한 경로와 실제 라우팅·파일 해석에 사용한 경로는 같은 정규형이어야 한다”**는 것이다. 네트워크 공격자는 인증 없이 경로 표현만 바꿔 경로 기반 정책이 보호하던 엔드포인트나 정적 자원에 접근할 수 있었다.
URL은 문자를 %와 두 자리 16진수로 표현할 수 있다. 예를 들어 i는 %69, 세미콜론은
%3B, 슬래시는 %2F다. RFC 3986은 문자를 크게 비예약 문자와 예약 문자로 나눈다.
비예약 문자: A-Z a-z 0-9 - . _ ~
예약 문자: : / ? # [ ] @ ! $ & ' ( ) * + , ; =
"/api/adm%69n" → i는 비예약 문자이므로 "/api/admin"과 같은 식별이 자연스럽다.
"/a%2Fb" → %2F를 "/"로 풀면 경로 segment 수가 바뀌므로 해석 시점이 중요하다.
"/a%3Bx=1" → %3B를 ";"로 풀면 matrix parameter 구문이 생길 수 있다.
Vert.x의 RoutingContext.normalizedPath()는 비예약 문자의 인코딩을 풀고 .·.. segment를
정리하지만, 예약 문자는 인코딩된 채 남긴다. 이는 라우팅 단계에서 예약 문자의 구조적
의미를 성급히 바꾸지 않기 위한 선택이다. 문제는 뒤의 컴포넌트가 더 많은 문자를 풀 때
생긴다. 보안 검사와 실제 사용 단계가 서로 다른 디코딩 규칙을 쓰면 같은 요청이 두 경로로
보일 수 있다.
Quarkus의 AbstractPathMatchingHttpSecurityPolicy는 설정된 경로 패턴을
ImmutablePathMatcher에 넣고, 요청마다 일치하는 HttpSecurityPolicy를 찾는다. 개념적으로
다음과 같은 설정은 /api/admin/ 아래 요청에 인증 정책을 적용한다.
quarkus.http.auth.permission.admin.paths=/api/admin/*
quarkus.http.auth.permission.admin.policy=authenticated요청 경로
│
▼
normalizedPath()
│
▼
matrix parameter 제거
│
▼
ImmutablePathMatcher.match()
│
├─ /api/admin/* 일치 → admin 정책 실행
└─ 불일치 → 이 정책은 선택되지 않음
여기서 경로 문자열은 단순한 로그용 값이 아니라 어떤 권한 검사기를 실행할지 고르는 보안
식별자다. 후단 핸들러가 다른 규칙으로 같은 요청을 /api/admin/...으로 해석한다면,
정책 선택이 끝난 뒤에 보호 경로가 다시 나타나는 셈이다.
세미콜론 뒤의 name=value는 경로 segment에 붙는 matrix parameter로 쓰일 수 있다.
예를 들어 /cars;color=red/parts;year=2026에서 실제 계층 경로는 /cars/parts이고,
나머지는 각 segment의 부가 정보다.
Quarkus 3.36.2의 pathWithoutMatrixParams()는 리터럴 세미콜론부터 다음 슬래시 전까지를
버린다.
public static String pathWithoutMatrixParams(String path) {
if (path.indexOf(';') == -1) {
return path;
}
StringBuilder sb = new StringBuilder(path.length());
boolean inMatrix = false;
for (int i = 0; i < path.length(); i++) {
char c = path.charAt(i);
if (c == ';') {
inMatrix = true;
} else if (c == '/') {
inMatrix = false;
sb.append(c);
} else if (!inMatrix) {
sb.append(c);
}
}
return sb.toString();
}/api/admin;bypass=true/data는 /api/admin/data가 되지만,
/api/admin%3Bbypass=true/data에는 리터럴 ;가 없다. 입력이 이 함수에 도달하기 전에
%3B가 풀리지 않았다면 함수는 원문을 그대로 반환한다.
Path Canonicalization은 여러 표현이 가리키는 경로를 하나의 정규형으로 만드는 작업이다. 보안 판단에서는 “검사한 뒤 더 해석하지 않는다”는 순서가 중요하다.
안전한 순서
raw path → 충분한 디코딩 → 구분자/점 segment 정리 → 권한 검사 → 같은 의미로 사용
위험한 순서
raw path → 일부 디코딩 → 권한 검사 → 추가 디코딩/구분자 변환 → 자원 사용
└─ 검사 때 없던 보호 경로가 생김
이 CVE는 두 번째 순서에 해당한다. 입력을 금지 문자 목록으로 막는 문제라기보다, 보안 계층과 소비 계층이 경로의 동일성을 다르게 판단한 문제다.
Quarkus 3.36.2의 정책 선택 코드는 Vert.x가 만든 부분 정규화 경로를
pathWithoutMatrixParams()에 바로 전달했다.
private static List<HttpMatcher> findHttpMatchers(
RoutingContext context,
ImmutablePathMatcher<List<HttpMatcher>> pathMatcher) {
String normalizedPath = context.normalizedPath();
PathMatch<List<HttpMatcher>> toCheck =
pathMatcher.match(HttpSecurityUtils.pathWithoutMatrixParams(normalizedPath));
// ...
}요청이 /api/admin%3Bbypass=true/data라면 normalizedPath()는 예약 문자인 %3B를
그대로 둔다. 다음 함수는 세미콜론을 찾지 못해 같은 문자열을 반환한다. 그 결과
/api/admin/* 정책과 일치하지 않는다.
후단 라우팅은 matrix parameter 의미를 적용해 세미콜론 뒤를 segment 부가 정보로 다룰 수 있다. 공식 권고에 따르면 이 불일치 때문에 최종 엔드포인트는 요청을 처리하지만 경로 기반 정책은 선택되지 않아, 해당 정책이 요구한 인증을 우회할 수 있었다.
[패치 전: encoded semicolon]
요청: /api/admin%3Bbypass=true/data
│
▼
Vert.x normalizedPath()
/api/admin%3Bbypass=true/data (%3B 유지)
│
▼
pathWithoutMatrixParams()
리터럴 ';' 없음 → 문자열 변화 없음
│
▼
보안 정책 matcher
/api/admin/* 와 불일치 → admin 정책 선택 안 됨
│
▼
후단 라우팅/경로 해석
%3B → ';' → matrix parameter 제거
/api/admin/data
│
▼
보호 대상 엔드포인트에 인증 정책 없이 도달
정적 자원 경로에서는 불일치가 한 단계 더 분명하다. 공식 권고는
StaticHandlerImpl과 FileSystemStaticHandler가
URIDecoder.decodeURIComponent()로 percent-encoding을 완전히 풀고, 역슬래시를
슬래시로 바꾼 뒤 파일 시스템 경로를 결정한다고 설명한다.
예를 들어 보안 정책이 /secret/confidential.html을 보호한다고 하자. 요청 경로가
/secret%2Fconfidential.html이면 보안 계층은 %2F를 남긴 문자열을 보므로 보호 패턴과
일치시키지 못할 수 있다. 정적 핸들러는 %2F를 /로 바꾼 뒤 자원을 해석한다.
%5C도 먼저 역슬래시가 되고 다시 슬래시로 바뀐다.
다중 인코딩도 같은 간격을 넓힌다. %252F는 첫 디코딩에서 %2F, 다음 디코딩에서 /가
된다. 보안 계층이 한 번만 부분 디코딩하고 후단이 다시 완전 디코딩하면, 권한 검사 이후에
새 경로 구분자가 만들어진다.
[패치 전: static resource와 이중 인코딩]
raw request: /secret%252Fconfidential.html
│
▼
보안 계층 normalizedPath()
/secret%2Fconfidential.html
(첫 단계에서 %25만 '%'로 바뀜)
│
▼
정책 matcher
정규 보호 경로와 불일치 → 정책 선택 안 됨
│
▼
정적 파일 핸들러의 추가 디코딩
%2F → /
/secret/confidential.html
│
▼
보호하려던 정적 자원을 읽을 수 있음
Quarkus REST(RESTEasy Reactive) 엔드포인트는 %2F·%5C 정적 파일 벡터의 영향을 받지
않는다. 공식 권고에 따르면 REST 라우팅 계층도 보안 계층과 같은 normalizedPath()를
사용하므로, 인코딩된 슬래시·역슬래시는 어느 REST route에도 일치하지 않는다. 반면
%3B matrix parameter 우회는 경로 정책으로 보호한 엔드포인트 전반에 영향을 줄 수 있다.
또한 인코딩된 점 segment %2e%2e는 .이 비예약 문자라 normalizedPath() 단계에서
이미 ..가 되고 제거되므로, 권고가 확인한 공격 벡터가 아니다. 이 구분은 “인코딩된
문자라면 모두 우회된다”는 과도한 일반화를 피하는 데 중요하다.
Quarkus 3.37.0은 HttpSecurityUtils.normalizePath()를 추가하고, 설정 경로를 matcher에
등록할 때와 요청 경로를 matcher에서 찾을 때 모두 이 함수를 사용한다.
public static String normalizePath(String path) {
while (path.indexOf('%') >= 0) {
String decoded = decodePercent(path);
if (decoded.equals(path)) {
break;
}
path = decoded;
}
path = pathWithoutMatrixParams(path);
if (path.indexOf('\0') >= 0) {
path = path.replace("\0", "");
}
if (path.indexOf('\\') >= 0) {
path = path.replace('\\', '/');
}
path = HttpUtils.removeDots(path);
return path;
}변환 순서는 의도적이다.
%가 남아 있고 디코딩이 진행되는 동안 반복해 다중 인코딩까지 푼다.%3B가 실제;가 된 뒤 matrix parameter를 제거한다.- NUL을 제거하고 역슬래시를 슬래시로 통일한다.
- 디코딩·구분자 변환 뒤 새로 생길 수 있는
.·..segment를 마지막에 정리한다.
호출 지점도 기존 pathWithoutMatrixParams()에서 새 함수로 바뀌었다.
String normalizedPath = context.normalizedPath();
PathMatch<List<HttpMatcher>> toCheck =
- pathMatcher.match(HttpSecurityUtils.pathWithoutMatrixParams(normalizedPath));
+ pathMatcher.match(HttpSecurityUtils.normalizePath(normalizedPath));설정된 정책 경로 역시 builder.addPath(normalizePath(path), ...)로 넣는다. 즉 matcher의
양쪽 입력이 같은 함수와 같은 순서를 거친다.
[패치 후]
설정 경로 ───────────────┐
▼
normalizePath()
│
▼
policy matcher
▲
│
요청 경로 ───────────────┘
① 반복 percent-decode
② matrix parameter 제거
③ NUL 제거
④ '\' → '/'
⑤ dot segment 제거
/api/admin%3Bbypass=true/data
→ /api/admin;bypass=true/data
→ /api/admin/data
→ /api/admin/* 정책과 일치
→ 인증/권한 검사 실행
/secret%252Fconfidential.html
→ /secret%2Fconfidential.html
→ /secret/confidential.html
→ 정적 핸들러가 볼 경로와 같은 의미로 정책 검사
3.37.0의 단위 테스트는 %3B, %2F, %5C, %00, 이중·삼중 인코딩, 역슬래시와
점 segment의 조합이 예상 정규형 하나로 수렴하는지 확인한다. 이 보고서는 테스트나
공개 요청 예제를 실행하지 않고 태그의 소스와 테스트를 정적으로 비교했다.
운영자는 사용하는 지원 계열에서 3.20.6.2, 3.27.4.1/3.27.5, 3.33.2.1/3.33.3,
3.36.3 또는 3.37.0 이상으로 업데이트해야 한다. Quarkus 공식 공지는 최신 Quarkus CLI로
quarkus update를 실행하는 방법을 안내한다. 임시로 경로 패턴만 더 추가하는 대응은
다중 인코딩과 후단별 해석 차이를 빠뜨릴 수 있으므로 정식 패치를 대신하지 못한다.
GitHub Security Advisory는 인코딩된 세미콜론·슬래시·역슬래시 요청 예제를 공개한다.
이 보고서는 해당 요청을 실행하지 않았으며, 예제가 보여 주는 문자열 변환을 공식 설명,
3.36.2/3.37.0 소스와 회귀 테스트로만 정적으로 대조했다. 실제 영향은 애플리케이션이
quarkus.http.auth.permission 경로 정책을 사용하고, 요청이 후단에서 보안 계층보다 더
강하게 디코딩되는 엔드포인트 또는 정적 자원으로 해석되는지에 따라 달라진다.
프록시·웹 프레임워크·애플리케이션이 연속으로 URL을 해석할 때 어느 계층이 유일한 정규형을 소유해야 할까?
다중 인코딩을 반복해서 푸는 정책은 우회를 막는 이점과 원래 리터럴 %를 보존해야 하는 호환성 사이에서 어떤 경계를 가져야 할까?
- GHSA-qcxp-gm7m-4j5v — Quarkus Security Advisory — 영향 범위, 공격 벡터, root cause와 수정 알고리즘
- CVE-2026-50559 — NVD — CVSS, CWE와 영향 버전 범위
- Quarkus 3.36.2
HttpSecurityUtils.java— 리터럴 세미콜론만 처리한 취약 구현, Apache License 2.0 - Quarkus 3.37.0
HttpSecurityUtils.java— 완전 정규화 구현, Apache License 2.0 - Quarkus 3.37.0 경로 정규화 단위 테스트 — 인코딩·구분자·점 segment 회귀 사례, Apache License 2.0
- Quarkus 긴급 릴리스 공지 — 지원 계열별 수정 버전과 업데이트 명령, CC BY 3.0
- RFC 3986 §2 — Characters — percent-encoding과 예약·비예약 문자 정의
본문 참고 자료로 충분했다.