davsclaus commented on code in PR #26026:
URL: https://github.com/apache/camel/pull/26026#discussion_r3910989387


##########
components/camel-avro/src/main/java/org/apache/camel/dataformat/avro/AvroDataFormat.java:
##########
@@ -135,6 +158,7 @@ protected Schema loadSchema(String className) throws 
CamelException, ClassNotFou
 
     @Override
     public void marshal(Exchange exchange, Object graph, OutputStream 
outputStream) throws Exception {
+        AvroClassSecuritySupport.trustClassName(graph.getClass().getName());

Review Comment:
   This unconditionally trusts the *outgoing* message body's runtime class — 
and, via `AvroClassSecuritySupport`'s prefix-matched package trust, its entire 
package — in a **JVM-wide, process-lifetime** allowlist shared by every 
CamelContext/route/data-format instance, with no way to revoke in production.
   
   If the class marshalled here can ever be influenced (directly or indirectly) 
by untrusted input in a route, that class/package becomes permanently accepted 
for Avro *unmarshalling* everywhere else in the same JVM, including unrelated 
routes/contexts — silently widening the allowlist that Avro 1.12.2's 
`ClassSecurityValidator` was added to enforce. Since `useSchema` already falls 
back to `loadSchema(graph.getClass().getName())` only when `actualSchema` is 
null, could the trust here be scoped to only the case where the schema/class 
was explicitly configured (data-format construction time), rather than 
re-trusting on every `marshal()` call based on whatever the exchange body 
happens to be at runtime?
   
   Given this touches the deserialization-security boundary, this seems worth a 
security-focused second look before merge.



##########
components/camel-avro/src/main/java/org/apache/camel/avro/support/AvroClassSecuritySupport.java:
##########
@@ -0,0 +1,152 @@
+/*
+ * 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.avro.support;
+
+import java.util.Arrays;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.avro.util.ClassSecurityValidator;
+import org.apache.avro.util.ClassSecurityValidator.ClassSecurityPredicate;
+
+/**
+ * Configures Apache Avro {@link ClassSecurityValidator} with Camel trusted 
packages.
+ * <p>
+ * Avro 1.12+ validates classes resolved from schemas. Camel automatically 
trusts packages derived from configured
+ * protocol or schema classes. Additional packages can be configured through 
the {@code serializablePackages} endpoint
+ * option.
+ */
+public final class AvroClassSecuritySupport {
+
+    private static final Set<String> TRUSTED_PACKAGES = 
ConcurrentHashMap.newKeySet();
+
+    private static final Set<String> TRUSTED_CLASSES = 
ConcurrentHashMap.newKeySet();
+
+    private static final Object LOCK = new Object();
+
+    private static final ClassSecurityPredicate CAMEL_TRUSTED = 
AvroClassSecuritySupport::isCamelTrusted;
+
+    private AvroClassSecuritySupport() {
+    }
+
+    /**
+     * Trusts Avro IPC classes required for camel-avro-rpc handshake.
+     */
+    public static void ensureAvroIpcPackagesTrusted() {
+        trustPackages("org.apache.avro.ipc");
+    }
+
+    /**
+     * Trusts the exact class name and its package for schema resolution.
+     */
+    public static void trustClassName(String className) {
+        if (className == null || className.isBlank()) {
+            return;
+        }
+        synchronized (LOCK) {
+            TRUSTED_CLASSES.add(className);
+            int lastDot = className.lastIndexOf('.');
+            if (lastDot > 0) {
+                TRUSTED_PACKAGES.add(normalizePackage(className.substring(0, 
lastDot)));
+            }
+            refreshGlobal();
+        }
+    }
+
+    /**
+     * Trusts the comma-separated list of packages.
+     */
+    public static void trustPackages(String packages) {
+        if (packages == null || packages.isBlank()) {
+            return;
+        }
+        trustPackages(parsePackages(packages).toArray(String[]::new));
+    }
+
+    /**
+     * Trusts the given packages.
+     */
+    public static void trustPackages(String... packages) {
+        if (packages == null || packages.length == 0) {
+            return;
+        }
+        synchronized (LOCK) {
+            for (String pkg : packages) {
+                if (pkg != null && !pkg.isBlank()) {
+                    TRUSTED_PACKAGES.add(normalizePackage(pkg));
+                }
+            }
+            refreshGlobal();
+        }
+    }
+
+    /**
+     * Clears Camel-managed trusted classes and packages. Intended for tests.
+     */
+    public static void resetForTesting() {
+        synchronized (LOCK) {
+            TRUSTED_PACKAGES.clear();
+            TRUSTED_CLASSES.clear();
+            ClassSecurityValidator.setGlobal(ClassSecurityValidator.DEFAULT);
+        }
+    }
+
+    private static void refreshGlobal() {
+        ClassSecurityValidator.setGlobal(
+                
ClassSecurityValidator.composite(ClassSecurityValidator.DEFAULT, 
CAMEL_TRUSTED));
+    }
+
+    private static boolean isCamelTrusted(Class<?> clazz) {
+        String className = clazz.getName();
+        if (TRUSTED_CLASSES.contains(className)) {
+            return true;
+        }
+        NavigableSet<String> packages = normalizedPackages(TRUSTED_PACKAGES);
+        String lower = packages.lower(className);
+        return lower != null && className.startsWith(lower);
+    }
+
+    private static Set<String> parsePackages(String packages) {
+        return Arrays.stream(packages.split(","))
+                .map(String::trim)
+                .filter(s -> !s.isEmpty())
+                .map(AvroClassSecuritySupport::normalizePackage)
+                
.collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new));
+    }
+
+    private static String normalizePackage(String pkg) {
+        String normalized = pkg.trim();
+        if ("*".equals(normalized)) {
+            throw new IllegalArgumentException(
+                    "Wildcard '*' is not supported in serializablePackages 
because it disables Avro class-loading protection");
+        }
+        if (normalized.endsWith(".")) {
+            normalized = normalized.substring(0, normalized.length() - 1);
+        }
+        return normalized;
+    }
+
+    private static NavigableSet<String> normalizedPackages(Set<String> 
packages) {

Review Comment:
   `normalizedPackages(TRUSTED_PACKAGES)` builds a brand-new `TreeSet` from 
scratch on every call to `isCamelTrusted`, which runs on every 
`ClassSecurityValidator.validate()` invocation — i.e. potentially once per 
class per Avro message during marshal/unmarshal. That's an avoidable allocation 
+ O(n log n) rebuild in a hot serialization path. Consider caching the sorted 
view and only rebuilding it when `TRUSTED_PACKAGES` actually changes (e.g. 
recompute inside `trustPackages`/`trustClassName` under the existing `LOCK`, 
store in a `volatile` field, and have `isCamelTrusted` just read it).



##########
components/camel-avro/src/main/java/org/apache/camel/avro/support/AvroClassSecuritySupport.java:
##########
@@ -0,0 +1,152 @@
+/*
+ * 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.avro.support;
+
+import java.util.Arrays;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.avro.util.ClassSecurityValidator;
+import org.apache.avro.util.ClassSecurityValidator.ClassSecurityPredicate;
+
+/**
+ * Configures Apache Avro {@link ClassSecurityValidator} with Camel trusted 
packages.
+ * <p>
+ * Avro 1.12+ validates classes resolved from schemas. Camel automatically 
trusts packages derived from configured
+ * protocol or schema classes. Additional packages can be configured through 
the {@code serializablePackages} endpoint
+ * option.
+ */
+public final class AvroClassSecuritySupport {
+
+    private static final Set<String> TRUSTED_PACKAGES = 
ConcurrentHashMap.newKeySet();
+
+    private static final Set<String> TRUSTED_CLASSES = 
ConcurrentHashMap.newKeySet();
+
+    private static final Object LOCK = new Object();
+
+    private static final ClassSecurityPredicate CAMEL_TRUSTED = 
AvroClassSecuritySupport::isCamelTrusted;
+
+    private AvroClassSecuritySupport() {
+    }
+
+    /**
+     * Trusts Avro IPC classes required for camel-avro-rpc handshake.
+     */
+    public static void ensureAvroIpcPackagesTrusted() {
+        trustPackages("org.apache.avro.ipc");
+    }
+
+    /**
+     * Trusts the exact class name and its package for schema resolution.
+     */
+    public static void trustClassName(String className) {
+        if (className == null || className.isBlank()) {
+            return;
+        }
+        synchronized (LOCK) {
+            TRUSTED_CLASSES.add(className);
+            int lastDot = className.lastIndexOf('.');
+            if (lastDot > 0) {
+                TRUSTED_PACKAGES.add(normalizePackage(className.substring(0, 
lastDot)));
+            }
+            refreshGlobal();
+        }
+    }
+
+    /**
+     * Trusts the comma-separated list of packages.
+     */
+    public static void trustPackages(String packages) {
+        if (packages == null || packages.isBlank()) {
+            return;
+        }
+        trustPackages(parsePackages(packages).toArray(String[]::new));
+    }
+
+    /**
+     * Trusts the given packages.
+     */
+    public static void trustPackages(String... packages) {
+        if (packages == null || packages.length == 0) {
+            return;
+        }
+        synchronized (LOCK) {
+            for (String pkg : packages) {
+                if (pkg != null && !pkg.isBlank()) {
+                    TRUSTED_PACKAGES.add(normalizePackage(pkg));
+                }
+            }
+            refreshGlobal();
+        }
+    }
+
+    /**
+     * Clears Camel-managed trusted classes and packages. Intended for tests.
+     */
+    public static void resetForTesting() {
+        synchronized (LOCK) {
+            TRUSTED_PACKAGES.clear();
+            TRUSTED_CLASSES.clear();
+            ClassSecurityValidator.setGlobal(ClassSecurityValidator.DEFAULT);
+        }
+    }
+
+    private static void refreshGlobal() {

Review Comment:
   `ClassSecurityValidator.setGlobal(...)` is unconditionally overwritten here 
whenever Camel trusts a new package/class. If the embedding application (or 
another library) had set its own custom global `ClassSecurityValidator`, this 
silently replaces it with `composite(DEFAULT, CAMEL_TRUSTED)`, dropping the 
application's own trust rules. Worth composing with whatever was previously 
installed (if it wasn't Camel's own) rather than always starting from `DEFAULT`.



##########
components/camel-avro/pom.xml:
##########
@@ -53,6 +48,11 @@
             <artifactId>camel-test-junit6</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>

Review Comment:
   Every existing test in `camel-avro` (e.g. 
`AvroGenericMarshalAndUnmarshalTest`, `AvroDateMarshalAndUnmarshalTest`) uses 
JUnit 5 `Assertions`, so this module has an established assertion-style 
convention. Per project convention, new tests should follow that existing style 
rather than introducing AssertJ as an outlier, and a new test dependency for 
two files' worth of style preference doesn't meet the "no new dependencies 
without justification" bar. Could `AvroClassSecuritySupportTest` be rewritten 
with `org.junit.jupiter.api.Assertions` (`assertThrows`, etc.) instead, and 
this dependency dropped?



##########
components/camel-avro-rpc/camel-avro-rpc-component/pom.xml:
##########
@@ -62,6 +61,11 @@
             <artifactId>camel-test-junit6</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>

Review Comment:
   Same as the note on `camel-avro/pom.xml`: this module's existing tests (e.g. 
`AvroSettingsTest`) use JUnit 5 assertions, so introducing `assertj-core` here 
just for `AvroClassSecurityWithoutVmArgsTest` is a style outlier and an 
unjustified new dependency. Consider using `org.junit.jupiter.api.Assertions` 
instead and dropping this dependency.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to