This is an automated email from the ASF dual-hosted git repository.

gabor pushed a commit to branch branch-1.11
in repository https://gitbox.apache.org/repos/asf/avro.git


The following commit(s) were added to refs/heads/branch-1.11 by this push:
     new 625395aa8f AVRO-4189: [java] Simplify the setting of the serializable 
classes (#3525)
625395aa8f is described below

commit 625395aa8f0a08ddd132124f82ed3e1dc5f1bb6f
Author: Gabor Szadovszky <[email protected]>
AuthorDate: Wed Oct 22 17:45:12 2025 +0200

    AVRO-4189: [java] Simplify the setting of the serializable classes (#3525)
    
    * AVRO-4189: [java] Simplify the setting of the serializable classes
    
    * Fix missing license header
    
    * Fix test failures + copilot findings
    
    * Fix system property settings in pomx
    
    (cherry picked from commit e0217aefdac4ca24d5b4500891c1898ad1677ae7)
---
 lang/java/avro/pom.xml                             |   3 -
 .../apache/avro/specific/SpecificDatumReader.java  |  95 ++------
 .../apache/avro/util/ClassSecurityValidator.java   | 254 +++++++++++++++++++++
 .../main/java/org/apache/avro/util/ClassUtils.java |  12 +-
 .../avro/reflect/TestReflectDatumReader.java       |  18 ++
 .../avro/specific/TestSpecificRecordWithUnion.java |  32 ++-
 .../avro/util/TestClassSecurityValidator.java      | 115 ++++++++++
 lang/java/ipc/pom.xml                              |   5 +-
 .../avro/mapred/tether/TestWordCountTether.java    |   1 +
 lang/java/pom.xml                                  |  24 ++
 .../java/org/apache/avro/tool/TestTetherTool.java  |   1 +
 11 files changed, 472 insertions(+), 88 deletions(-)

diff --git a/lang/java/avro/pom.xml b/lang/java/avro/pom.xml
index 3ef5d02ea8..45e6662bb4 100644
--- a/lang/java/avro/pom.xml
+++ b/lang/java/avro/pom.xml
@@ -72,9 +72,6 @@
         <artifactId>maven-surefire-plugin</artifactId>
         <configuration>
           <parallel>none</parallel>
-          <systemProperties>
-            
<org.apache.avro.SERIALIZABLE_CLASSES>java.math.BigDecimal,java.math.BigInteger,java.net.URI,java.net.URL,java.io.File,java.lang.Integer,org.apache.avro.reflect.TestReflect$R10</org.apache.avro.SERIALIZABLE_CLASSES>
-          </systemProperties>
         </configuration>
         <executions>
           <execution>
diff --git 
a/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumReader.java
 
b/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumReader.java
index 510c9fed84..ca9da138c3 100644
--- 
a/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumReader.java
+++ 
b/lang/java/avro/src/main/java/org/apache/avro/specific/SpecificDatumReader.java
@@ -22,15 +22,13 @@ import org.apache.avro.Schema;
 import org.apache.avro.AvroRuntimeException;
 import org.apache.avro.generic.GenericDatumReader;
 import org.apache.avro.io.ResolvingDecoder;
+import org.apache.avro.util.ClassSecurityValidator.SystemPropertiesPredicate;
 import org.apache.avro.util.ClassUtils;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.stream.Stream;
+import org.apache.avro.util.ClassSecurityValidator;
 
 /**
  * {@link org.apache.avro.io.DatumReader DatumReader} for generated Java
@@ -39,47 +37,20 @@ import java.util.stream.Stream;
 public class SpecificDatumReader<T> extends GenericDatumReader<T> {
 
   /**
-   * @deprecated prefer to use {@link #SERIALIZABLE_CLASSES} instead.
+   * @deprecated Use {@link SystemPropertiesPredicate} instead.
+   * @see ClassSecurityValidator
    */
   @Deprecated
-  public static final String[] SERIALIZABLE_PACKAGES;
-
-  public static final String[] SERIALIZABLE_CLASSES;
-
-  static {
-    // no serializable classes by default
-    SERIALIZABLE_CLASSES = 
streamPropertyEntries(System.getProperty("org.apache.avro.SERIALIZABLE_CLASSES"))
-        .toArray(String[]::new);
-
-    // no serializable packages by default
-    SERIALIZABLE_PACKAGES = 
streamPropertyEntries(System.getProperty("org.apache.avro.SERIALIZABLE_PACKAGES"))
-        // Add a '.' suffix to ensure we'll be matching package names instead 
of
-        // arbitrary prefixes, except for the wildcard "*", which allows all
-        // packages (this is only safe in fully controlled environments!).
-        .map(entry -> "*".equals(entry) ? entry : entry + 
".").toArray(String[]::new);
-  }
+  public static final String[] SERIALIZABLE_PACKAGES = 
SystemPropertiesPredicate.SERIALIZABLE_PACKAGES
+      .toArray(new String[0]);
 
   /**
-   * Parse a comma separated list into non-empty entries. Leading and trailing
-   * whitespace is stripped.
-   *
-   * @param commaSeparatedEntries the comma separated list of entries
-   * @return a stream of the entries
+   * @deprecated Use {@link SystemPropertiesPredicate} instead.
+   * @see ClassSecurityValidator
    */
-  private static Stream<String> streamPropertyEntries(String 
commaSeparatedEntries) {
-    if (commaSeparatedEntries == null) {
-      return Stream.empty();
-    }
-    return 
Stream.of(commaSeparatedEntries.split(",")).map(String::trim).filter(s -> 
!s.isEmpty());
-  }
-
-  // The primitive "class names" based on Class.isPrimitive()
-  private static final Set<String> PRIMITIVES = new 
HashSet<>(Arrays.asList(Boolean.TYPE.getName(),
-      Character.TYPE.getName(), Byte.TYPE.getName(), Short.TYPE.getName(), 
Integer.TYPE.getName(), Long.TYPE.getName(),
-      Float.TYPE.getName(), Double.TYPE.getName(), Void.TYPE.getName()));
-
-  private final List<String> trustedPackages = new ArrayList<>();
-  private final List<String> trustedClasses = new ArrayList<>();
+  @Deprecated
+  public static final String[] SERIALIZABLE_CLASSES = 
SystemPropertiesPredicate.SERIALIZABLE_CLASSES
+      .toArray(new String[0]);
 
   public SpecificDatumReader() {
     this(null, null, SpecificData.get());
@@ -106,15 +77,11 @@ public class SpecificDatumReader<T> extends 
GenericDatumReader<T> {
    */
   public SpecificDatumReader(Schema writer, Schema reader, SpecificData data) {
     super(writer, reader, data);
-    trustedPackages.addAll(Arrays.asList(SERIALIZABLE_PACKAGES));
-    trustedClasses.addAll(Arrays.asList(SERIALIZABLE_CLASSES));
   }
 
   /** Construct given a {@link SpecificData}. */
   public SpecificDatumReader(SpecificData data) {
     super(data);
-    trustedPackages.addAll(Arrays.asList(SERIALIZABLE_PACKAGES));
-    trustedClasses.addAll(Arrays.asList(SERIALIZABLE_CLASSES));
   }
 
   /** Return the contained {@link SpecificData}. */
@@ -156,7 +123,6 @@ public class SpecificDatumReader<T> extends 
GenericDatumReader<T> {
     if (name == null)
       return null;
     try {
-      checkSecurity(name);
       Class clazz = ClassUtils.forName(getData().getClassLoader(), name);
       return clazz;
     } catch (ClassNotFoundException e) {
@@ -164,43 +130,22 @@ public class SpecificDatumReader<T> extends 
GenericDatumReader<T> {
     }
   }
 
-  private boolean trustAllPackages() {
-    return (trustedPackages.size() == 1 && "*".equals(trustedPackages.get(0)));
-  }
-
-  private void checkSecurity(String className) throws ClassNotFoundException {
-    if (trustAllPackages() || PRIMITIVES.contains(className)) {
-      return;
-    }
-
-    for (String trustedClass : getTrustedClasses()) {
-      if (className.equals(trustedClass)) {
-        return;
-      }
-    }
-
-    for (String trustedPackage : getTrustedPackages()) {
-      if (className.startsWith(trustedPackage)) {
-        return;
-      }
-    }
-
-    throw new SecurityException("Forbidden " + className + "! This class is 
not trusted to be included in Avro "
-        + "schemas using java-class. Please set the system property 
org.apache.avro.SERIALIZABLE_CLASSES to the comma "
-        + "separated list of classes you trust. You can also set the system 
property "
-        + "org.apache.avro.SERIALIZABLE_PACKAGES to the comma separated list 
of the packages you trust.");
-  }
-
   /**
-   * @deprecated Use getTrustedClasses() instead
+   * @deprecated Use {@link SystemPropertiesPredicate} instead.
+   * @see ClassSecurityValidator
    */
   @Deprecated
   public final List<String> getTrustedPackages() {
-    return trustedPackages;
+    return Arrays.asList(SERIALIZABLE_PACKAGES);
   }
 
+  /**
+   * @deprecated Use {@link SystemPropertiesPredicate} instead.
+   * @see ClassSecurityValidator
+   */
+  @Deprecated
   public final List<String> getTrustedClasses() {
-    return trustedClasses;
+    return Arrays.asList(SERIALIZABLE_CLASSES);
   }
 
   @Override
diff --git 
a/lang/java/avro/src/main/java/org/apache/avro/util/ClassSecurityValidator.java 
b/lang/java/avro/src/main/java/org/apache/avro/util/ClassSecurityValidator.java
new file mode 100644
index 0000000000..b50d0f0250
--- /dev/null
+++ 
b/lang/java/avro/src/main/java/org/apache/avro/util/ClassSecurityValidator.java
@@ -0,0 +1,254 @@
+/*
+ * 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
+ *
+ *     https://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.avro.util;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.NavigableSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Validates that a class is trusted to be included in Avro schemas. To be used
+ * by {@link ClassUtils} which therefore automatically guards not only the
+ * loading of the classes but, since the class names are translated into
+ * {@link Class} objects by using {@link ClassUtils}, also guards any other
+ * reflection-based mechanisms (e.g. instantiation, setting internal 
variables).
+ *
+ * @see #setGlobal(ClassSecurityPredicate)
+ * @see #getGlobal()
+ */
+public final class ClassSecurityValidator {
+
+  /**
+   * Validates that the class is trusted to be included in Avro schemas.
+   *
+   * <p>
+   * Note: this method shall be invoked with un-initialized classes only to
+   * prevent any potential security issues the initialization may trigger.
+   *
+   * @param clazz the class to validate
+   * @throws SecurityException if the class is not trusted
+   */
+  public static void validate(Class<?> clazz) {
+    while (clazz.isArray()) {
+      clazz = clazz.getComponentType();
+    }
+    if (clazz.isPrimitive()) {
+      return;
+    }
+    if (!globalInstance.isTrusted(clazz)) {
+      globalInstance.forbiddenClass(clazz.getName());
+    }
+  }
+
+  /**
+   * Sets the global {@link ClassSecurityPredicate} that is used by
+   * {@link ClassUtils} to validate the trusted classes.
+   *
+   * @param validator the validator to use
+   */
+  public static void setGlobal(ClassSecurityPredicate validator) {
+    globalInstance = Objects.requireNonNull(validator);
+  }
+
+  /**
+   * Returns the global {@link ClassSecurityPredicate} that is used by
+   * {@link ClassUtils} to validate the trusted classes.
+   *
+   * @return the global validator
+   */
+  public static ClassSecurityPredicate getGlobal() {
+    return globalInstance;
+  }
+
+  private ClassSecurityValidator() {
+  }
+
+  /**
+   * A predicate that checks if a class is trusted to be included in Avro 
schemas.
+   */
+  public interface ClassSecurityPredicate {
+    /**
+     * Checks if the class is trusted to be included in Avro schemas.
+     *
+     * @param clazz the class to check
+     * @return true if the class is trusted, false otherwise
+     */
+    boolean isTrusted(Class<?> clazz);
+
+    /**
+     * Throws a {@link SecurityException} with a message indicating that the 
class
+     * is not trusted to be included in Avro schemas.
+     *
+     * @param className the name of the class that is not trusted
+     */
+    default void forbiddenClass(String className) {
+      throw new SecurityException("Forbidden " + className + "! This class is 
not trusted to be included in Avro "
+          + "schemas. You may either use the system properties 
org.apache.avro.SERIALIZABLE_CLASSES and "
+          + "org.apache.avro.SERIALIZABLE_PACKAGES to set the comma separated 
list of the classes or packages you trust, "
+          + "or you can set them via the API (see 
org.apache.avro.util.ClassSecurityValidator).");
+    }
+  }
+
+  /**
+   * A couple of trusted classes that are safe to be loaded, instantiated with 
any
+   * constructors or alter any internals via reflection.
+   */
+  public static final ClassSecurityPredicate DEFAULT_TRUSTED_CLASSES = 
builder().add("java.lang.Boolean")
+      
.add("java.lang.Byte").add("java.lang.Character").add("java.lang.CharSequence").add("java.lang.Double")
+      
.add("java.lang.Enum").add("java.lang.Float").add("java.lang.Integer").add("java.lang.Long")
+      
.add("java.lang.Number").add("java.lang.Object").add("java.lang.Short").add("java.lang.String")
+      
.add("java.lang.Void").add("java.math.BigDecimal").add("java.math.BigInteger").build();
+
+  /**
+   * The predicate that uses the system properties
+   * {@value SystemPropertiesPredicate#SYSPROP_SERIALIZABLE_CLASSES} and
+   * {@value SystemPropertiesPredicate#SYSPROP_SERIALIZABLE_PACKAGES}.
+   */
+  public static final ClassSecurityPredicate SYSTEM_PROPERTIES = new 
SystemPropertiesPredicate();
+
+  /**
+   * The default predicate that uses both the system properties and the 
hard-coded
+   * trusted classes.
+   *
+   * @see #DEFAULT_TRUSTED_CLASSES
+   * @see #SYSTEM_PROPERTIES
+   */
+  public static final ClassSecurityPredicate DEFAULT = 
composite(DEFAULT_TRUSTED_CLASSES, SYSTEM_PROPERTIES);
+
+  private static ClassSecurityPredicate globalInstance = DEFAULT;
+
+  /**
+   * Creates a builder for a {@link ClassSecurityValidator} that validates the
+   * trusted classes by whitelisting their names. Note: no parent validator is
+   * used.
+   *
+   * @return a new {@link ClassSecurityValidator} builder
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /**
+   * Creates a composite {@link ClassSecurityValidator} that delegates to the
+   * given validators.
+   *
+   * @param validators the validators to delegate to
+   * @return a new {@link ClassSecurityValidator} that delegates to the given
+   *         validators
+   */
+  public static ClassSecurityPredicate composite(ClassSecurityPredicate... 
validators) {
+    return clazz -> Arrays.stream(validators).anyMatch(v -> 
v.isTrusted(clazz));
+  }
+
+  public static class Builder {
+    private final Set<String> allowedClassNames = new HashSet<>();
+
+    private Builder() {
+    }
+
+    public Builder add(String className) {
+      allowedClassNames.add(className);
+      return this;
+    }
+
+    public Builder add(Class<?> clazz) {
+      return add(clazz.getName());
+    }
+
+    public ClassSecurityPredicate build() {
+      return clazz -> allowedClassNames.contains(clazz.getName());
+    }
+  }
+
+  public static class SystemPropertiesPredicate implements 
ClassSecurityPredicate {
+
+    /**
+     * The set of trusted classes specified by the system property
+     * {@value #SYSPROP_SERIALIZABLE_CLASSES}. Empty by default.
+     */
+    public static final Set<String> SERIALIZABLE_CLASSES;
+
+    /**
+     * The set of trusted packages specified by the system property
+     * {@value #SYSPROP_SERIALIZABLE_PACKAGES}. Empty by default.
+     */
+    public static final NavigableSet<String> SERIALIZABLE_PACKAGES;
+
+    private static final boolean TRUST_ALL_PACKAGES;
+
+    private static final String SYSPROP_SERIALIZABLE_CLASSES = 
"org.apache.avro.SERIALIZABLE_CLASSES";
+
+    private static final String SYSPROP_SERIALIZABLE_PACKAGES = 
"org.apache.avro.SERIALIZABLE_PACKAGES";
+
+    static {
+      // add the hard-coded trusted classes as well
+      SERIALIZABLE_CLASSES = Collections.unmodifiableSet(
+          
streamPropertyEntries(System.getProperty(SYSPROP_SERIALIZABLE_CLASSES)).collect(Collectors.toSet()));
+
+      // no default serializable packages are hard-coded
+      NavigableSet<String> packages = 
streamPropertyEntries(System.getProperty(SYSPROP_SERIALIZABLE_PACKAGES))
+          // Add a '.' suffix to ensure we'll be matching package names 
instead of
+          // arbitrary prefixes, except for the wildcard "*", which allows all
+          // packages (this is only safe in fully controlled environments!).
+          .map(entry -> "*".equals(entry) ? entry : entry + 
".").collect(TreeSet::new, TreeSet::add, TreeSet::addAll);
+      TRUST_ALL_PACKAGES = packages.remove("*");
+
+      SERIALIZABLE_PACKAGES = Collections.unmodifiableNavigableSet(packages);
+    }
+
+    /**
+     * Parse a comma separated list into non-empty entries. Leading and 
trailing
+     * whitespace is stripped.
+     *
+     * @param commaSeparatedEntries the comma separated list of entries
+     * @return a stream of the entries
+     */
+    private static Stream<String> streamPropertyEntries(String 
commaSeparatedEntries) {
+      if (commaSeparatedEntries == null) {
+        return Stream.empty();
+      }
+      return Stream.of(commaSeparatedEntries.split(",")).map(s -> 
s.replaceAll("^\\s+|\\s+$", ""))
+          .filter(s -> !s.isEmpty());
+    }
+
+    private SystemPropertiesPredicate() {
+    }
+
+    @Override
+    public boolean isTrusted(Class<?> clazz) {
+      if (TRUST_ALL_PACKAGES) {
+        return true;
+      }
+
+      String className = clazz.getName();
+
+      if (SERIALIZABLE_CLASSES.contains(className)) {
+        return true;
+      }
+
+      String lower = SERIALIZABLE_PACKAGES.lower(className);
+      return lower != null && className.startsWith(lower);
+    }
+  }
+}
diff --git a/lang/java/avro/src/main/java/org/apache/avro/util/ClassUtils.java 
b/lang/java/avro/src/main/java/org/apache/avro/util/ClassUtils.java
index dad59a551d..c21f276d6d 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/util/ClassUtils.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/util/ClassUtils.java
@@ -52,7 +52,7 @@ public class ClassUtils {
       c = forName(className, Thread.currentThread().getContextClassLoader());
     }
     if (c == null) {
-      throw new ClassNotFoundException("Failed to load class" + className);
+      throw new ClassNotFoundException("Failed to load class " + className);
     }
     return c;
   }
@@ -75,14 +75,14 @@ public class ClassUtils {
       c = forName(className, Thread.currentThread().getContextClassLoader());
     }
     if (c == null) {
-      throw new ClassNotFoundException("Failed to load class" + className);
+      throw new ClassNotFoundException("Failed to load class " + className);
     }
     return c;
   }
 
   /**
    * Loads a {@link Class} from the specified {@link ClassLoader} without 
throwing
-   * {@link ClassNotFoundException}.
+   * {@link ClassNotFoundException}. The class is loaded without 
initialization.
    *
    * @param className
    * @param classLoader
@@ -92,7 +92,11 @@ public class ClassUtils {
     Class<?> c = null;
     if (classLoader != null && className != null) {
       try {
-        c = Class.forName(className, true, classLoader);
+        // Load the class without initializing it so we can distinguish between
+        // ClassNotFoundException and SecurityException (that may be thrown by 
the
+        // validator).
+        c = Class.forName(className, false, classLoader);
+        ClassSecurityValidator.validate(c);
       } catch (ClassNotFoundException e) {
         // Ignore and return null
       }
diff --git 
a/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectDatumReader.java
 
b/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectDatumReader.java
index 3b97eab046..b403b44ef8 100644
--- 
a/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectDatumReader.java
+++ 
b/lang/java/avro/src/test/java/org/apache/avro/reflect/TestReflectDatumReader.java
@@ -19,6 +19,7 @@
 package org.apache.avro.reflect;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -33,6 +34,8 @@ import org.apache.avro.io.Decoder;
 import org.apache.avro.io.DecoderFactory;
 import org.apache.avro.io.Encoder;
 import org.apache.avro.io.EncoderFactory;
+import org.apache.avro.util.ClassSecurityValidator;
+import org.apache.avro.util.ClassSecurityValidator.ClassSecurityPredicate;
 import org.junit.jupiter.api.Test;
 
 public class TestReflectDatumReader {
@@ -47,6 +50,21 @@ public class TestReflectDatumReader {
     return byteArrayOutputStream.toByteArray();
   }
 
+  /**
+   * Test that the deserialization of a class that is not trusted throws a
+   * SecurityException.
+   */
+  @Test
+  void testNotSerializableClasses() {
+    ClassSecurityPredicate originalValidator = 
ClassSecurityValidator.getGlobal();
+    try {
+      
ClassSecurityValidator.setGlobal(ClassSecurityValidator.builder().build());
+      assertThrows(SecurityException.class, () -> new 
ReflectDatumReader<>(PojoWithArray.class));
+    } finally {
+      ClassSecurityValidator.setGlobal(originalValidator);
+    }
+  }
+
   @Test
   public void testRead_PojoWithList() throws IOException {
     PojoWithList pojoWithList = new PojoWithList();
diff --git 
a/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificRecordWithUnion.java
 
b/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificRecordWithUnion.java
index c3b330b28c..ba9dbe7607 100644
--- 
a/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificRecordWithUnion.java
+++ 
b/lang/java/avro/src/test/java/org/apache/avro/specific/TestSpecificRecordWithUnion.java
@@ -29,6 +29,9 @@ import org.apache.avro.io.DatumReader;
 import org.apache.avro.io.DatumWriter;
 import org.apache.avro.io.BinaryEncoder;
 import org.apache.avro.io.Decoder;
+
+import org.apache.avro.util.ClassSecurityValidator;
+import org.apache.avro.util.ClassSecurityValidator.ClassSecurityPredicate;
 import org.junit.Test;
 
 import java.io.ByteArrayInputStream;
@@ -37,8 +40,30 @@ import java.io.IOException;
 import java.math.BigDecimal;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 
 public class TestSpecificRecordWithUnion {
+  /**
+   * Test that the deserialization of a class that is not trusted throws a
+   * SecurityException.
+   */
+  @Test
+  public void testNotSerializableClasses() throws IOException {
+    final TestUnionRecord record = 
TestUnionRecord.newBuilder().setAmount(BigDecimal.ZERO).build();
+    final Schema schema = 
SchemaBuilder.unionOf().nullType().and().type(record.getSchema()).endUnion();
+
+    byte[] recordBytes = serializeRecord(
+        "{ \"org.apache.avro.specific.TestUnionRecord\": { \"amount\": { 
\"bytes\": \"\\u0000\" } } }", schema);
+
+    ClassSecurityPredicate originalValidator = 
ClassSecurityValidator.getGlobal();
+    try {
+      
ClassSecurityValidator.setGlobal(ClassSecurityValidator.builder().build());
+      assertThrows(SecurityException.class, () -> deserializeRecord(schema, 
recordBytes));
+    } finally {
+      ClassSecurityValidator.setGlobal(originalValidator);
+    }
+  }
+
   @Test
   public void testUnionLogicalDecimalConversion() throws IOException {
     final TestUnionRecord record = 
TestUnionRecord.newBuilder().setAmount(BigDecimal.ZERO).build();
@@ -47,11 +72,14 @@ public class TestSpecificRecordWithUnion {
     byte[] recordBytes = serializeRecord(
         "{ \"org.apache.avro.specific.TestUnionRecord\": { \"amount\": { 
\"bytes\": \"\\u0000\" } } }", schema);
 
+    assertEquals(record, deserializeRecord(schema, recordBytes));
+  }
+
+  private static SpecificRecord deserializeRecord(Schema schema, byte[] 
recordBytes) throws IOException {
     SpecificDatumReader<SpecificRecord> specificDatumReader = new 
SpecificDatumReader<>(schema);
     ByteArrayInputStream byteArrayInputStream = new 
ByteArrayInputStream(recordBytes);
     Decoder decoder = DecoderFactory.get().binaryDecoder(byteArrayInputStream, 
null);
-    final SpecificRecord deserialized = specificDatumReader.read(null, 
decoder);
-    assertEquals(record, deserialized);
+    return specificDatumReader.read(null, decoder);
   }
 
   public static byte[] serializeRecord(String value, Schema schema) throws 
IOException {
diff --git 
a/lang/java/avro/src/test/java/org/apache/avro/util/TestClassSecurityValidator.java
 
b/lang/java/avro/src/test/java/org/apache/avro/util/TestClassSecurityValidator.java
new file mode 100644
index 0000000000..4dc1a55c54
--- /dev/null
+++ 
b/lang/java/avro/src/test/java/org/apache/avro/util/TestClassSecurityValidator.java
@@ -0,0 +1,115 @@
+/*
+ * 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
+ *
+ *     https://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.avro.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.math.BigInteger;
+import org.apache.avro.util.ClassSecurityValidator.ClassSecurityPredicate;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestClassSecurityValidator {
+
+  // To test inner classes
+  private static class TestInnerClass {
+  }
+
+  private ClassSecurityPredicate originalValidator;
+
+  @BeforeEach
+  public void saveOriginalValidator() {
+    originalValidator = ClassSecurityValidator.getGlobal();
+  }
+
+  @AfterEach
+  public void restoreOriginalValidator() {
+    ClassSecurityValidator.setGlobal(originalValidator);
+  }
+
+  @Test
+  void testDefault() {
+    // Test a couple of default trusted classes via ClassUtils
+    assertDoesNotThrow(() -> 
ClassUtils.forName(boolean[][][][][][].class.getName()));
+    assertDoesNotThrow(() -> ClassUtils.forName("java.lang.String"));
+    assertDoesNotThrow(() -> 
ClassUtils.forName(java.math.BigDecimal[][][][].class.getName()));
+
+    // The package "org.apache.avro" is allowed by default for the test 
environment
+    assertDoesNotThrow(() -> 
ClassUtils.forName("org.apache.avro.util.TestClassSecurityValidator$TestInnerClass"));
+
+    // Test a couple of default untrusted classes via ClassUtils
+    assertThrows(SecurityException.class, () -> 
ClassUtils.forName("java.net.InetAddress"));
+    assertThrows(SecurityException.class, () -> 
ClassUtils.forName("java.io.FileInputStream"));
+  }
+
+  @Test
+  void testBuilder() {
+    
ClassSecurityValidator.setGlobal(ClassSecurityValidator.builder().add(TestClassSecurityValidator.class).build());
+
+    assertDoesNotThrow(() -> 
ClassUtils.forName("org.apache.avro.util.TestClassSecurityValidator"));
+    assertThrows(SecurityException.class,
+        () -> 
ClassUtils.forName("org.apache.avro.util.TestClassSecurityValidator$TestInnerClass"));
+
+    // Test that arrays and primitives also work
+    assertDoesNotThrow(() -> 
ClassUtils.forName(short[][][][][].class.getName()));
+    assertDoesNotThrow(() -> 
ClassUtils.forName(TestClassSecurityValidator[][][][].class.getName()));
+    assertThrows(SecurityException.class, () -> 
ClassUtils.forName(TestInnerClass[][].class.getName()));
+  }
+
+  @Test
+  void testOwnImplementation() {
+    ClassSecurityValidator.setGlobal(new ClassSecurityPredicate() {
+      @Override
+      public boolean isTrusted(Class<?> clazz) {
+        return clazz.getSimpleName().contains("Inner");
+      }
+
+      @Override
+      public void forbiddenClass(String className) {
+        throw new SecurityException("Not inner");
+      }
+    });
+    assertDoesNotThrow(() -> 
ClassUtils.forName(TestInnerClass.class.getName()));
+    Exception e = assertThrows(SecurityException.class,
+        () -> ClassUtils.forName(TestClassSecurityValidator.class.getName()));
+    assertEquals("Not inner", e.getMessage());
+
+    // Test that arrays and primitives also work
+    assertDoesNotThrow(() -> ClassUtils.forName(char[][][][].class.getName()));
+    assertDoesNotThrow(() -> 
ClassUtils.forName(TestInnerClass[][][][].class.getName()));
+    e = assertThrows(SecurityException.class, () -> 
ClassUtils.forName(TestClassSecurityValidator[][].class.getName()));
+    assertEquals("Not inner", e.getMessage());
+  }
+
+  @Test
+  void testBuildComplexPredicate() {
+    ClassSecurityValidator.setGlobal(ClassSecurityValidator.composite(
+        
ClassSecurityValidator.builder().add(TestInnerClass.class).add(TestClassSecurityValidator.class).build(),
+        ClassSecurityValidator.DEFAULT, c -> 
c.getPackageName().equals("java.lang")));
+
+    // Test that the defaults work since we included them
+    testDefault();
+
+    assertDoesNotThrow(() -> 
ClassUtils.forName(TestInnerClass.class.getName()));
+    assertDoesNotThrow(() -> 
ClassUtils.forName(TestClassSecurityValidator.class.getName()));
+    assertDoesNotThrow(() -> 
ClassUtils.forName(StringBuilder.class.getName()));
+    assertDoesNotThrow(() -> ClassUtils.forName("java.lang.StringBuffer"));
+    assertDoesNotThrow(() -> ClassUtils.forName(BigInteger.class.getName()));
+  }
+}
diff --git a/lang/java/ipc/pom.xml b/lang/java/ipc/pom.xml
index ef7275f88b..675fc5ee1e 100644
--- a/lang/java/ipc/pom.xml
+++ b/lang/java/ipc/pom.xml
@@ -61,10 +61,7 @@
           <!-- some tests hang if not run in a separate JVM -->
           <forkCount>1</forkCount>
           <reuseForks>false</reuseForks>
-           <parallel>none</parallel>
-          <systemProperties>
-            
<org.apache.avro.SERIALIZABLE_CLASSES>java.math.BigDecimal,java.math.BigInteger</org.apache.avro.SERIALIZABLE_CLASSES>
-          </systemProperties>
+          <parallel>none</parallel>
         </configuration>
       </plugin>
       <plugin>
diff --git 
a/lang/java/mapred/src/test/java/org/apache/avro/mapred/tether/TestWordCountTether.java
 
b/lang/java/mapred/src/test/java/org/apache/avro/mapred/tether/TestWordCountTether.java
index 3af07713ba..040c2b1241 100644
--- 
a/lang/java/mapred/src/test/java/org/apache/avro/mapred/tether/TestWordCountTether.java
+++ 
b/lang/java/mapred/src/test/java/org/apache/avro/mapred/tether/TestWordCountTether.java
@@ -79,6 +79,7 @@ public class TestWordCountTether {
     List<String> execargs = new ArrayList<>();
     execargs.add("-classpath");
     execargs.add(System.getProperty("java.class.path"));
+    execargs.add("-Dorg.apache.avro.SERIALIZABLE_PACKAGES=org.apache.avro");
     execargs.add("org.apache.avro.mapred.tether.WordCountTask");
 
     FileInputFormat.addInputPaths(job, inputPath.toString());
diff --git a/lang/java/pom.xml b/lang/java/pom.xml
index d2ac4440c2..124c9b0307 100644
--- a/lang/java/pom.xml
+++ b/lang/java/pom.xml
@@ -189,6 +189,30 @@
             <redirectTestOutputToFile>true</redirectTestOutputToFile>
             <failIfNoTests>false</failIfNoTests>
             <argLine>-Xmx1000m</argLine>
+            <systemPropertyVariables>
+
+              <!-- Using the related system properties to set the trusted 
classes/packages for all tests (in every module). -->
+              <org.apache.avro.SERIALIZABLE_CLASSES>
+                java.net.URI,
+                java.net.URL,
+                java.io.File,
+                java.util.HashMap,
+                java.util.List,
+                java.util.Collection,
+                java.util.Map,
+                java.util.Set,
+                java.util.concurrent.ConcurrentHashMap,
+                java.util.LinkedHashMap,
+                java.util.TreeMap,
+                example.avro.Bar,
+                com.google.protobuf.Timestamp
+              </org.apache.avro.SERIALIZABLE_CLASSES>
+              <org.apache.avro.SERIALIZABLE_PACKAGES>
+                org.apache.avro,
+                test
+              </org.apache.avro.SERIALIZABLE_PACKAGES>
+
+            </systemPropertyVariables>
           </configuration>
         </plugin>
         <plugin>
diff --git 
a/lang/java/tools/src/test/java/org/apache/avro/tool/TestTetherTool.java 
b/lang/java/tools/src/test/java/org/apache/avro/tool/TestTetherTool.java
index d453069074..333461bc4d 100644
--- a/lang/java/tools/src/test/java/org/apache/avro/tool/TestTetherTool.java
+++ b/lang/java/tools/src/test/java/org/apache/avro/tool/TestTetherTool.java
@@ -78,6 +78,7 @@ public class TestTetherTool {
 
     // create a string of the arguments
     String execargs = "-classpath " + System.getProperty("java.class.path");
+    execargs += " -Dorg.apache.avro.SERIALIZABLE_PACKAGES=org.apache.avro";
     execargs += " org.apache.avro.mapred.tether.WordCountTask";
 
     // Create a list of the arguments to pass to the tull run method

Reply via email to