This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new e66985e993e7 CAMEL-24937: camel-pqc - confine
FileBasedKeyLifecycleManager key file resolution to the configured directory
e66985e993e7 is described below
commit e66985e993e74ce0d361be177fe45d7ddfb8bcdb
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Sep 28 18:48:21 2026 +0200
CAMEL-24937: camel-pqc - confine FileBasedKeyLifecycleManager key file
resolution to the configured directory
FileBasedKeyLifecycleManager built key file paths by appending the keyId,
which can come from the CamelPQCKeyId / CamelPQCNewKeyId headers, to the
key directory. A keyId such as ../victim could therefore read, overwrite
or delete a file outside the key directory.
A keyId must now be a flat file name: one containing a forward slash, a
backslash or a NUL character is rejected with an IllegalArgumentException,
and the resolved path is checked to stay inside the (absolute, normalized)
key directory. Deployments that used a keyId like tenant-a/signing to place
keys in a subdirectory must switch to a flat keyId; the upgrade guide
describes it. The in-memory and cloud-backed managers are not affected.
Closes #26782
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-authored-by: Guillaume Nodet - AI Bot <[email protected]>
---
.../lifecycle/FileBasedKeyLifecycleManager.java | 36 ++++++++--
.../FileBasedKeyLifecycleManagerPathTest.java | 77 ++++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 13 ++++
3 files changed, 121 insertions(+), 5 deletions(-)
diff --git
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
index 33901921c08f..8d4b43f67e2b 100644
---
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
+++
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
@@ -67,7 +67,7 @@ public class FileBasedKeyLifecycleManager implements
KeyLifecycleManager {
private final ConcurrentHashMap<String, KeyMetadata> metadataCache = new
ConcurrentHashMap<>();
public FileBasedKeyLifecycleManager(String keyDirectoryPath) throws
IOException {
- this.keyDirectory = Paths.get(keyDirectoryPath);
+ this.keyDirectory =
Paths.get(keyDirectoryPath).toAbsolutePath().normalize();
this.objectMapper = new ObjectMapper();
this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
Files.createDirectories(keyDirectory);
@@ -442,19 +442,45 @@ public class FileBasedKeyLifecycleManager implements
KeyLifecycleManager {
}
private Path getPrivateKeyFile(String keyId) {
- return keyDirectory.resolve(keyId + ".private.json");
+ return resolveKeyFile(keyId, ".private.json");
}
private Path getPublicKeyFile(String keyId) {
- return keyDirectory.resolve(keyId + ".public.json");
+ return resolveKeyFile(keyId, ".public.json");
}
private Path getMetadataFile(String keyId) {
- return keyDirectory.resolve(keyId + ".metadata");
+ return resolveKeyFile(keyId, ".metadata");
}
private Path getLegacyKeyFile(String keyId) {
- return keyDirectory.resolve(keyId + ".key");
+ return resolveKeyFile(keyId, ".key");
+ }
+
+ /**
+ * Resolves a key file inside the configured key directory, confining it
to that directory. keyId values arrive from
+ * message headers (CamelPQCKeyId / CamelPQCNewKeyId), so a value
containing path separators, parent references or
+ * an absolute path could otherwise resolve to a location outside the key
directory. keyId is therefore constrained
+ * to a single flat file-name segment, and the resolved path is checked to
lie textually under the key directory
+ * (which is stored absolute and normalized in the constructor).
+ * <p/>
+ * The {@code startsWith} check is textual and does not follow symbolic
links, so a symlink placed inside the key
+ * directory could still point elsewhere. Creating such a link requires
write access to the operator-controlled key
+ * directory, which is outside the header-supplied keyId threat this
guards against.
+ */
+ Path resolveKeyFile(String keyId, String suffix) {
+ if (keyId == null || keyId.isBlank()) {
+ throw new IllegalArgumentException("keyId must not be null or
empty");
+ }
+ if (keyId.indexOf('/') >= 0 || keyId.indexOf('\\') >= 0 ||
keyId.indexOf('\0') >= 0) {
+ throw new IllegalArgumentException("keyId must not contain path
separators (length: " + keyId.length() + ")");
+ }
+ Path resolved = keyDirectory.resolve(keyId + suffix).normalize();
+ if (!resolved.startsWith(keyDirectory)) {
+ throw new IllegalArgumentException(
+ "keyId must resolve inside the key directory (length: " +
keyId.length() + ")");
+ }
+ return resolved;
}
/**
diff --git
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManagerPathTest.java
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManagerPathTest.java
new file mode 100644
index 000000000000..6d54536b5877
--- /dev/null
+++
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManagerPathTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.pqc.lifecycle;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * keyId values reach the file-based key store from message headers
(CamelPQCKeyId / CamelPQCNewKeyId). The manager must
+ * keep every key file inside its configured directory regardless of the
supplied keyId.
+ */
+class FileBasedKeyLifecycleManagerPathTest {
+
+ @Test
+ void rejectsKeyIdWithParentTraversal(@TempDir Path tempDir) throws
Exception {
+ Path keyDir = tempDir.resolve("keys");
+ FileBasedKeyLifecycleManager manager = new
FileBasedKeyLifecycleManager(keyDir.toString());
+
+ // A sentinel file one level above the key directory that an
unconfined "../victim" keyId would target
+ Path sentinel = tempDir.resolve("victim.private.json");
+ Files.writeString(sentinel, "{}");
+
+ assertThrows(IllegalArgumentException.class, () ->
manager.getKey("../victim"));
+ assertThrows(IllegalArgumentException.class, () ->
manager.deleteKey("../victim"));
+
+ assertTrue(Files.exists(sentinel), "a file outside the key directory
must not be reachable via keyId");
+ }
+
+ @Test
+ void rejectsAbsoluteSeparatorAndBlankKeyIds(@TempDir Path tempDir) throws
Exception {
+ Path keyDir = tempDir.resolve("keys");
+ FileBasedKeyLifecycleManager manager = new
FileBasedKeyLifecycleManager(keyDir.toString());
+
+ assertThrows(IllegalArgumentException.class, () ->
manager.getKeyMetadata("sub/evil"));
+ assertThrows(IllegalArgumentException.class, () ->
manager.getKeyMetadata("/etc/evil"));
+ assertThrows(IllegalArgumentException.class,
+ () ->
manager.getKeyMetadata(tempDir.resolve("abs").toString()));
+ assertThrows(IllegalArgumentException.class, () ->
manager.getKeyMetadata(""));
+ // NUL character — caught by character guard
+ assertThrows(IllegalArgumentException.class, () ->
manager.getKeyMetadata("evil\0inject"));
+ }
+
+ @Test
+ void allowsPlainKeyId(@TempDir Path tempDir) throws Exception {
+ Path keyDir = tempDir.resolve("keys");
+ FileBasedKeyLifecycleManager manager = new
FileBasedKeyLifecycleManager(keyDir.toString());
+
+ // A normal flat keyId is accepted: metadata for an absent key returns
null rather than being rejected
+ assertNull(manager.getKeyMetadata("tenant-a-signing-key"));
+ // and its key file resolves directly inside the key directory
+ Path resolved = manager.resolveKeyFile("tenant-a-signing-key",
".private.json");
+ assertEquals(keyDir.toAbsolutePath().normalize(),
resolved.getParent());
+ assertEquals("tenant-a-signing-key.private.json",
resolved.getFileName().toString());
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 22e3d3ae84a2..96ade35bd1bf 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -3147,6 +3147,18 @@ are unaffected. Routes that set the header by its
literal string name, or that u
`allowTemplateFromHeader=true` with the old header names, must switch to the
new `Camel`-prefixed
names.
+=== camel-pqc - FileBasedKeyLifecycleManager restricts keyId to a flat file
name
+
+`FileBasedKeyLifecycleManager` now confines every key file to its configured
key directory. A `keyId`
+(supplied through the `CamelPQCKeyId` / `CamelPQCNewKeyId` headers) that
contains a forward slash, a
+backslash, a null character, or that would otherwise resolve outside the key
directory is now rejected
+with an `IllegalArgumentException`.
+
+Previously a `keyId` such as `tenant-a/signing` resolved into a subdirectory
of the key directory.
+Deployments that organised keys that way must switch to a flat `keyId` (for
example `tenant-a-signing`).
+Only the file-based manager is affected; the in-memory and cloud-backed
managers were never file-path
+based.
+
=== camel-crypto
Three changes to `CryptoDataFormat`, none of which affects the format of data
already written.
@@ -3172,6 +3184,7 @@ allocation; a declared length outside 0–1024 is now
rejected instead of attemp
Not changed: the HMAC key is still derived from the same key material as the
cipher. Separating them
would change the MAC written into the message and so could not be read by
earlier versions; that is
tracked separately.
+
=== camel-debezium - a failed embedded engine is now reported
The Debezium consumers now register a `CompletionCallback` on the embedded
engine. When the engine stops