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

mawiesne pushed a commit to branch opennlp-2.x
in repository https://gitbox.apache.org/repos/asf/opennlp.git


The following commit(s) were added to refs/heads/opennlp-2.x by this push:
     new 1b6bba291 [2.x] OPENNLP-1891: Harden Deserialization in BaseModel 
(#1186)
1b6bba291 is described below

commit 1b6bba291f11c3ce906830037ac4c4a7ddae7c31
Author: Richard Zowalla <[email protected]>
AuthorDate: Sat Jul 18 13:45:10 2026 +0200

    [2.x] OPENNLP-1891: Harden Deserialization in BaseModel (#1186)
    
    * Backport of #1185 to the 2.x branch. Adds a filtered 
BaseModel.deserialize(Class, InputStream)
    helper that installs an ObjectInputFilter allowlist before reading, since a 
filter cannot be
    installed from within readObject for the top-level object.
    
    * Javadoc adapted for 2.x, which has
    no SvmDoccatModel.
    
    * Train test model on demand instead of using a 1.x model file
    
    ---------
    
    Co-authored-by: subbudvk <[email protected]>
---
 .../java/opennlp/tools/util/model/BaseModel.java   | 40 +++++++++-
 .../opennlp/tools/util/model/BaseModelTest.java    | 90 ++++++++++++++++++++++
 2 files changed, 129 insertions(+), 1 deletion(-)

diff --git 
a/opennlp-tools/src/main/java/opennlp/tools/util/model/BaseModel.java 
b/opennlp-tools/src/main/java/opennlp/tools/util/model/BaseModel.java
index 35e477634..5660a3a33 100644
--- a/opennlp-tools/src/main/java/opennlp/tools/util/model/BaseModel.java
+++ b/opennlp-tools/src/main/java/opennlp/tools/util/model/BaseModel.java
@@ -24,6 +24,7 @@ import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.ObjectInputFilter;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.OutputStream;
@@ -685,9 +686,46 @@ public abstract class BaseModel implements 
ArtifactProvider, Serializable {
     this.serialize(out);
   }
 
+  /**
+   * Allowlist for classes that may legitimately appear while deserializing a
+   * {@link BaseModel} via the standard {@link Serializable} mechanism.
+   */
+  private static final ObjectInputFilter DESERIALIZE_FILTER = 
ObjectInputFilter.Config.createFilter(
+      "opennlp.tools.**;" +
+      "java.util.HashMap;" +
+      "java.lang.String;" +
+      "!*");
+
+  /**
+   * Deserializes a {@link BaseModel} from the given {@link InputStream} using 
the
+   * standard {@link Serializable} mechanism, with {@link #DESERIALIZE_FILTER} 
applied
+   * to reject any class outside the allowlist - including the top-level 
object itself.
+   * <p>
+   * Prefer this method (or the {@code BaseModel(String, 
InputStream|File|Path|URL)}
+   * constructors) over calling {@code new ObjectInputStream(in).readObject()} 
directly,
+   * since a filter installed inside {@link #readObject} cannot protect the 
top-level
+   * object - the JDK only allows an {@link ObjectInputFilter} to be set on a 
stream
+   * before the first object is read.
+   *
+   * @param modelClass The expected {@link BaseModel} subclass.
+   * @param in The {@link InputStream} to read from. Must not be {@code null}.
+   * @param <T> The type of the expected model.
+   * @return A valid {@link BaseModel} instance of type {@code T}.
+   *
+   * @throws IOException Thrown if IO errors occurred, including a rejection by
+   *                      {@link #DESERIALIZE_FILTER}.
+   * @throws ClassNotFoundException Thrown if required classes are not found.
+   */
+  public static <T extends BaseModel> T deserialize(Class<T> modelClass, 
InputStream in)
+      throws IOException, ClassNotFoundException {
+    try (ObjectInputStream ois = new ObjectInputStream(in)) {
+      ois.setObjectInputFilter(DESERIALIZE_FILTER);
+      return modelClass.cast(ois.readObject());
+    }
+  }
+
   @Serial
   private void readObject(final ObjectInputStream in) throws IOException {
-
     isLoadedFromSerialized = true;
     artifactSerializers = new HashMap<>();
     artifactMap = new HashMap<>();
diff --git 
a/opennlp-tools/src/test/java/opennlp/tools/util/model/BaseModelTest.java 
b/opennlp-tools/src/test/java/opennlp/tools/util/model/BaseModelTest.java
new file mode 100644
index 000000000..641b300c0
--- /dev/null
+++ b/opennlp-tools/src/test/java/opennlp/tools/util/model/BaseModelTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 opennlp.tools.util.model;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InvalidClassException;
+import java.io.ObjectOutputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import opennlp.tools.chunker.ChunkSample;
+import opennlp.tools.chunker.ChunkSampleStream;
+import opennlp.tools.chunker.ChunkerFactory;
+import opennlp.tools.chunker.ChunkerME;
+import opennlp.tools.chunker.ChunkerModel;
+import opennlp.tools.formats.ResourceAsStreamFactory;
+import opennlp.tools.util.ObjectStream;
+import opennlp.tools.util.PlainTextByLineStream;
+import opennlp.tools.util.TrainingParameters;
+
+/**
+ * Tests {@link BaseModel#deserialize(Class, java.io.InputStream)} and the
+ * {@link java.io.ObjectInputFilter} it installs.
+ */
+public class BaseModelTest {
+
+  private static ChunkerModel model;
+
+  @BeforeAll
+  static void trainModel() throws IOException {
+    ResourceAsStreamFactory in = new 
ResourceAsStreamFactory(BaseModelTest.class,
+        "/opennlp/tools/chunker/test.txt");
+
+    ObjectStream<ChunkSample> sampleStream = new ChunkSampleStream(
+        new PlainTextByLineStream(in, StandardCharsets.UTF_8));
+
+    TrainingParameters params = new TrainingParameters();
+    params.put(TrainingParameters.ITERATIONS_PARAM, 5);
+    params.put(TrainingParameters.CUTOFF_PARAM, 1);
+
+    model = ChunkerME.train("eng", sampleStream, params, new ChunkerFactory());
+  }
+
+  @Test
+  void testDeserializeRoundTrip() throws Exception {
+
+    final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    try (ObjectOutputStream oos = new ObjectOutputStream(bytesOut)) {
+      oos.writeObject(model);
+    }
+
+    final ChunkerModel deserialized = BaseModel.deserialize(
+        ChunkerModel.class, new ByteArrayInputStream(bytesOut.toByteArray()));
+    Assertions.assertNotNull(deserialized);
+    Assertions.assertEquals(model.getLanguage(), deserialized.getLanguage());
+  }
+
+  @Test
+  void testDeserializeRejectsDisallowedTopLevelClass() throws Exception {
+    final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    try (ObjectOutputStream oos = new ObjectOutputStream(bytesOut)) {
+      // Not on the DESERIALIZE_FILTER allowlist - simulates an attacker 
substituting
+      // an entirely different top-level class for the expected model.
+      oos.writeObject(new java.util.ArrayList<>());
+    }
+
+    Assertions.assertThrows(InvalidClassException.class, () ->
+        BaseModel.deserialize(ChunkerModel.class, new 
ByteArrayInputStream(bytesOut.toByteArray())));
+  }
+}

Reply via email to