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

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new cffd027b89 fix: don't resolve a wire-supplied manifest class the 
serializer will not use (#3495)
cffd027b89 is described below

commit cffd027b89937115ee9a6a7c5b9f8e37df53a6cd
Author: PJ Fanning <[email protected]>
AuthorDate: Mon Aug 31 22:01:39 2026 +0100

    fix: don't resolve a wire-supplied manifest class the serializer will not 
use (#3495)
    
    Motivation:
    Serialization.deserializeByteArray resolved the manifest string from the 
wire
    into a Class via dynamicAccess.getClassFor for every plain Serializer, 
without
    checking whether that serializer wants a type hint. Serializers.manifestFor
    shows a conforming peer only sends a manifest when includeManifest is true, 
so
    for a serializer declaring includeManifest = false the class name can only 
have
    come from a non-conforming or hostile sender - and the loaded class is then
    discarded. ByteArraySerializer and NullSerializer are bound by default and
    reachable by serializer id, so a peer could name any class on the classpath 
and
    have it loaded: a classpath-probing oracle, and metaspace and manifest-cache
    growth that is never released.
    
    Modification:
    Pass None to a plain Serializer that declares includeManifest = false 
instead of
    resolving the wire-supplied name, which is exactly what a conforming sender
    produces. Serializers that do ask for the hint are unchanged.
    
    Result:
    A peer can no longer drive class loading through a serializer that ignores 
the
    type hint. No behaviour change for conforming messages.
    
    Tests:
    - sbt "actor-tests/testOnly 
org.apache.pekko.serialization.WireManifestClassLoadingSpec" - new; the first 
case fails without this change with "Cannot find manifest class 
[com.example.NotOnTheClasspath]"
    - sbt "actor-tests/testOnly org.apache.pekko.serialization.SerializeSpec" - 
existing spec passes unchanged
    - sbt "actor/mimaReportBinaryIssues" - no issues
    
    References:
    Refs #3478
---
 .../WireManifestClassLoadingSpec.scala             | 85 ++++++++++++++++++++++
 .../apache/pekko/serialization/Serialization.scala |  6 +-
 2 files changed, 90 insertions(+), 1 deletion(-)

diff --git 
a/actor-tests/src/test/scala/org/apache/pekko/serialization/WireManifestClassLoadingSpec.scala
 
b/actor-tests/src/test/scala/org/apache/pekko/serialization/WireManifestClassLoadingSpec.scala
new file mode 100644
index 0000000000..1cf16a7ca6
--- /dev/null
+++ 
b/actor-tests/src/test/scala/org/apache/pekko/serialization/WireManifestClassLoadingSpec.scala
@@ -0,0 +1,85 @@
+/*
+ * 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.pekko.serialization
+
+import java.io.NotSerializableException
+import java.nio.charset.StandardCharsets.UTF_8
+
+import org.apache.pekko
+import pekko.actor.ExtendedActorSystem
+import pekko.testkit.PekkoSpec
+
+object WireManifestClassLoadingSpec {
+
+  /** A plain `Serializer` that does not want a type hint. */
+  class NoManifestSerializer(@annotation.nowarn("msg=never used") val system: 
ExtendedActorSystem) extends Serializer {
+    override def identifier: Int = 9911
+    override def includeManifest: Boolean = false
+    override def toBinary(o: AnyRef): Array[Byte] = o.toString.getBytes(UTF_8)
+    override def fromBinary(bytes: Array[Byte], manifest: Option[Class[?]]): 
AnyRef = {
+      // a serializer declaring includeManifest = false must never be handed a 
type hint
+      if (manifest.isDefined)
+        throw new AssertionError(s"unexpected type hint 
[${manifest.get.getName}]")
+      new String(bytes, UTF_8)
+    }
+  }
+
+  /** A plain `Serializer` that does want a type hint. */
+  class WithManifestSerializer(@annotation.nowarn("msg=never used") val 
system: ExtendedActorSystem)
+      extends Serializer {
+    override def identifier: Int = 9912
+    override def includeManifest: Boolean = true
+    override def toBinary(o: AnyRef): Array[Byte] = o.toString.getBytes(UTF_8)
+    override def fromBinary(bytes: Array[Byte], manifest: Option[Class[?]]): 
AnyRef =
+      new String(bytes, UTF_8) + ":" + 
manifest.map(_.getName).getOrElse("none")
+  }
+}
+
+class WireManifestClassLoadingSpec
+    extends PekkoSpec(s"""
+    pekko.actor.serializers {
+      no-manifest = 
"${classOf[WireManifestClassLoadingSpec.NoManifestSerializer].getName}"
+      with-manifest = 
"${classOf[WireManifestClassLoadingSpec.WithManifestSerializer].getName}"
+    }
+  """) {
+
+  private val serialization = SerializationExtension(system)
+  private val payload = "hello".getBytes(UTF_8)
+
+  "Deserialization of a wire-supplied manifest" must {
+
+    "not resolve a class for a serializer that declares includeManifest = 
false" in {
+      // A hostile or non-conforming peer can put any string in the manifest 
field. For a
+      // serializer that ignores the hint there is no reason to turn it into a 
class load.
+      val result = serialization.deserialize(payload, 9911, 
"com.example.NotOnTheClasspath").get
+      result should ===("hello")
+    }
+
+    "still resolve a class for a serializer that declares includeManifest = 
true" in {
+      val result = serialization.deserialize(payload, 9912, 
classOf[String].getName).get
+      result should ===("hello:java.lang.String")
+    }
+
+    "still fail for an unknown manifest class when the serializer wants the 
hint" in {
+      val ex = intercept[NotSerializableException] {
+        serialization.deserialize(payload, 9912, 
"com.example.NotOnTheClasspath").get
+      }
+      ex.getMessage should include("com.example.NotOnTheClasspath")
+    }
+  }
+}
diff --git 
a/actor/src/main/scala/org/apache/pekko/serialization/Serialization.scala 
b/actor/src/main/scala/org/apache/pekko/serialization/Serialization.scala
index 9515ec1b23..2443a28600 100644
--- a/actor/src/main/scala/org/apache/pekko/serialization/Serialization.scala
+++ b/actor/src/main/scala/org/apache/pekko/serialization/Serialization.scala
@@ -206,7 +206,11 @@ class Serialization(val system: ExtendedActorSystem) 
extends Extension {
       serializer match {
         case s2: SerializerWithStringManifest => s2.fromBinary(bytes, manifest)
         case s1                               =>
-          if (manifest == "")
+          if (manifest == "" || !s1.includeManifest)
+            // A conforming peer only sends a manifest for a serializer that 
asks for one
+            // (see `Serializers.manifestFor`), and a serializer with 
`includeManifest = false`
+            // ignores the type hint anyway. Resolving a class named by the 
wire that will not
+            // be used only exposes class loading to whoever wrote the message.
             s1.fromBinary(bytes, None)
           else {
             val cache = manifestCache.get


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to