Removing, deprecating and replacing usage of black/whitelist (#29254)

This commit is contained in:
Johan Andrén 2020-06-18 15:48:28 +02:00 committed by GitHub
parent 25ea7b7f5e
commit 1e9e984727
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 168 additions and 144 deletions

View file

@ -411,7 +411,7 @@ In order to force the `validatePullRequest` task to build the entire project, re
changes one can use the special `PLS BUILD ALL` command (typed in a comment on GitHub, on the pull request), which will cause changes one can use the special `PLS BUILD ALL` command (typed in a comment on GitHub, on the pull request), which will cause
the validator to test all projects. the validator to test all projects.
Note, that `OK TO TEST` will only be picked up when the user asking for it is considered an admin. Public (!) members of the [akka organization](https://github.com/orgs/akka/people) are automatically considered admins and users manually declared admin in the Jenkins job (currently no one is explicitly listed). `PLS BUILD` and `PLS BUILD ALL` can be issued by everyone that is an admin or everyone who was whitelisted in the Jenkins Job (whitelisting != declaring someone an admin). Note, that `OK TO TEST` will only be picked up when the user asking for it is considered an admin. Public (!) members of the [akka organization](https://github.com/orgs/akka/people) are automatically considered admins and users manually declared admin in the Jenkins job (currently no one is explicitly listed). `PLS BUILD` and `PLS BUILD ALL` can be issued by everyone that is an admin or everyone who was given permission in the Jenkins Job.
## Source style ## Source style

View file

@ -793,11 +793,7 @@ akka {
} }
serialization.protobuf { serialization.protobuf {
# deprecated, use `allowed-classes` instead
# Additional classes that are allowed even if they are not defined in `serialization-bindings`.
# It can be exact class name or name of super class or interfaces (one level).
# This is useful when a class is not used for serialization any more and therefore removed
# from `serialization-bindings`, but should still be possible to deserialize.
whitelist-class = [ whitelist-class = [
"com.google.protobuf.GeneratedMessage", "com.google.protobuf.GeneratedMessage",
"com.google.protobuf.GeneratedMessageV3", "com.google.protobuf.GeneratedMessageV3",
@ -805,6 +801,13 @@ akka {
"akka.protobuf.GeneratedMessage", "akka.protobuf.GeneratedMessage",
"akka.protobufv3.internal.GeneratedMessageV3" "akka.protobufv3.internal.GeneratedMessageV3"
] ]
# Additional classes that are allowed even if they are not defined in `serialization-bindings`.
# It can be exact class name or name of super class or interfaces (one level).
# This is useful when a class is not used for serialization any more and therefore removed
# from `serialization-bindings`, but should still be possible to deserialize.
allowed-classes = ${akka.serialization.protobuf.whitelist-class}
} }
# Used to set the behavior of the scheduler. # Used to set the behavior of the scheduler.

View file

@ -71,7 +71,7 @@ private[cluster] class ClusterRemoteWatcher(
override val log = Logging(context.system, ActorWithLogClass(this, ClusterLogClass.ClusterCore)) override val log = Logging(context.system, ActorWithLogClass(this, ClusterLogClass.ClusterCore))
// allowed to watch even though address not in cluster membership, i.e. remote watch // allowed to watch even though address not in cluster membership, i.e. remote watch
private val watchPathWhitelist = Set("/system/sharding/") private val watchPathAllowList = Set("/system/sharding/")
private var pendingDelayedQuarantine: Set[UniqueAddress] = Set.empty private var pendingDelayedQuarantine: Set[UniqueAddress] = Set.empty
@ -177,7 +177,7 @@ private[cluster] class ClusterRemoteWatcher(
private def isWatchOutsideClusterAllowed(watchee: InternalActorRef): Boolean = { private def isWatchOutsideClusterAllowed(watchee: InternalActorRef): Boolean = {
context.system.name == watchee.path.address.system && { context.system.name == watchee.path.address.system && {
val pathPrefix = watchee.path.elements.take(2).mkString("/", "/", "/") val pathPrefix = watchee.path.elements.take(2).mkString("/", "/", "/")
watchPathWhitelist.contains(pathPrefix) watchPathAllowList.contains(pathPrefix)
} }
} }

View file

@ -105,14 +105,14 @@ The procedure for changing from Java serialization to Jackson would look like:
described in @ref:[Serialization with Jackson](../serialization-jackson.md). described in @ref:[Serialization with Jackson](../serialization-jackson.md).
* Test the system with the new serialization in a new test cluster (no rolling update). * Test the system with the new serialization in a new test cluster (no rolling update).
* Remove the binding for the marker interface in `akka.actor.serialization-bindings`, so that Jackson is not used for serialization (toBinary) yet. * Remove the binding for the marker interface in `akka.actor.serialization-bindings`, so that Jackson is not used for serialization (toBinary) yet.
* Configure `akka.serialization.jackson.whitelist-class-prefix=["com.myapp"]` * Configure `akka.serialization.jackson.allowed-class-prefix=["com.myapp"]`
* This is needed for Jackson deserialization when the `serialization-bindings` isn't defined. * This is needed for Jackson deserialization when the `serialization-bindings` isn't defined.
* Replace `com.myapp` with the name of the root package of your application to trust all classes. * Replace `com.myapp` with the name of the root package of your application to trust all classes.
* Roll out the change. * Roll out the change.
* Java serialization is still used, but this version is prepared for next roll out. * Java serialization is still used, but this version is prepared for next roll out.
1. Rolling update to enable serialization with Jackson. 1. Rolling update to enable serialization with Jackson.
* Add the binding to the marker interface in `akka.actor.serialization-bindings` to the Jackson serializer. * Add the binding to the marker interface in `akka.actor.serialization-bindings` to the Jackson serializer.
* Remove `akka.serialization.jackson.whitelist-class-prefix`. * Remove `akka.serialization.jackson.allowed-class-prefix`.
* Roll out the change. * Roll out the change.
* Old nodes will still send messages with Java serialization, and that can still be deserialized by new nodes. * Old nodes will still send messages with Java serialization, and that can still be deserialized by new nodes.
* New nodes will send messages with Jackson serialization, and old node can deserialize those because they were * New nodes will send messages with Jackson serialization, and old node can deserialize those because they were

View file

@ -271,22 +271,22 @@ Scala
Java Java
: @@snip [RemoteDeploymentDocTest.java](/akka-docs/src/test/java/jdocs/remoting/RemoteDeploymentDocTest.java) { #deploy } : @@snip [RemoteDeploymentDocTest.java](/akka-docs/src/test/java/jdocs/remoting/RemoteDeploymentDocTest.java) { #deploy }
### Remote deployment whitelist ### Remote deployment allow list
As remote deployment can potentially be abused by both users and even attackers a whitelist feature As remote deployment can potentially be abused by both users and even attackers an allow list feature
is available to guard the ActorSystem from deploying unexpected actors. Please note that remote deployment is available to guard the ActorSystem from deploying unexpected actors. Please note that remote deployment
is *not* remote code loading, the Actors class to be deployed onto a remote system needs to be present on that is *not* remote code loading, the Actors class to be deployed onto a remote system needs to be present on that
remote system. This still however may pose a security risk, and one may want to restrict remote deployment to remote system. This still however may pose a security risk, and one may want to restrict remote deployment to
only a specific set of known actors by enabling the whitelist feature. only a specific set of known actors by enabling the allow list feature.
To enable remote deployment whitelisting set the `akka.remote.deployment.enable-whitelist` value to `on`. To enable remote deployment allow list set the `akka.remote.deployment.enable-allow-list` value to `on`.
The list of allowed classes has to be configured on the "remote" system, in other words on the system onto which The list of allowed classes has to be configured on the "remote" system, in other words on the system onto which
others will be attempting to remote deploy Actors. That system, locally, knows best which Actors it should or others will be attempting to remote deploy Actors. That system, locally, knows best which Actors it should or
should not allow others to remote deploy onto it. The full settings section may for example look like this: should not allow others to remote deploy onto it. The full settings section may for example look like this:
@@snip [RemoteDeploymentWhitelistSpec.scala](/akka-remote/src/test/scala/akka/remote/classic/RemoteDeploymentWhitelistSpec.scala) { #whitelist-config } @@snip [RemoteDeploymentAllowListSpec.scala](/akka-remote/src/test/scala/akka/remote/classic/RemoteDeploymentAllowListSpec.scala) { #allow-list-config }
Actor classes not included in the whitelist will not be allowed to be remote deployed onto this system. Actor classes not included in the allow list will not be allowed to be remote deployed onto this system.
## Lifecycle and Failure Recovery Model ## Lifecycle and Failure Recovery Model

View file

@ -19,7 +19,7 @@ to ensure that a fix can be provided without delay.
## Security Related Documentation ## Security Related Documentation
* @ref:[Java Serialization](../serialization.md#java-serialization) * @ref:[Java Serialization](../serialization.md#java-serialization)
* @ref:[Remote deployment whitelist](../remoting.md#remote-deployment-whitelist) * @ref:[Remote deployment allow list](../remoting.md#remote-deployment-allow-list)
* @ref:[Remote Security](../remoting-artery.md#remote-security) * @ref:[Remote Security](../remoting-artery.md#remote-security)
## Fixed Security Vulnerabilities ## Fixed Security Vulnerabilities

View file

@ -69,7 +69,7 @@ such as:
* `java.io.Serializable` * `java.io.Serializable`
* `java.util.Comparable`. * `java.util.Comparable`.
The blacklist of possible serialization gadget classes defined by Jackson databind are checked The deny list of possible serialization gadget classes defined by Jackson databind are checked
and disallowed for deserialization. and disallowed for deserialization.
@@@ warning @@@ warning
@ -350,12 +350,12 @@ That type of migration must be configured with the old class name as key. The ac
### Remove from serialization-bindings ### Remove from serialization-bindings
When a class is not used for serialization any more it can be removed from `serialization-bindings` but to still When a class is not used for serialization any more it can be removed from `serialization-bindings` but to still
allow deserialization it must then be listed in the `whitelist-class-prefix` configuration. This is useful for example allow deserialization it must then be listed in the `allowed-class-prefix` configuration. This is useful for example
during rolling update with serialization changes, or when reading old stored data. It can also be used during rolling update with serialization changes, or when reading old stored data. It can also be used
when changing from Jackson serializer to another serializer (e.g. Protobuf) and thereby changing the serialization when changing from Jackson serializer to another serializer (e.g. Protobuf) and thereby changing the serialization
binding, but it should still be possible to deserialize old data with Jackson. binding, but it should still be possible to deserialize old data with Jackson.
@@snip [config](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #whitelist-class-prefix } @@snip [config](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #allowed-class-prefix }
It's a list of class names or prefixes of class names. It's a list of class names or prefixes of class names.

View file

@ -28,13 +28,13 @@ Java
: @@snip [StatefulMapConcat.java](/akka-docs/src/test/java/jdocs/stream/operators/flow/StatefulMapConcat.java) { #zip-with-index } : @@snip [StatefulMapConcat.java](/akka-docs/src/test/java/jdocs/stream/operators/flow/StatefulMapConcat.java) { #zip-with-index }
In this sample we let the value of the elements have an effect on the following elements, if an element starts In this sample we let the value of the elements have an effect on the following elements, if an element starts
with `blacklist:word` we add it to a black list and filter out any subsequent entries of `word`: with `deny:word` we add it to a deny list and filter out any subsequent entries of `word`:
Scala Scala
: @@snip [StatefulMapConcat.scala](/akka-docs/src/test/scala/docs/stream/operators/flow/StatefulMapConcat.scala) { #blacklist } : @@snip [StatefulMapConcat.scala](/akka-docs/src/test/scala/docs/stream/operators/flow/StatefulMapConcat.scala) { #denylist }
Java Java
: @@snip [StatefulMapConcat.java](/akka-docs/src/test/java/jdocs/stream/operators/flow/StatefulMapConcat.java) { #blacklist } : @@snip [StatefulMapConcat.java](/akka-docs/src/test/java/jdocs/stream/operators/flow/StatefulMapConcat.java) { #denylist }
For cases where there is a need to emit elements based on the state when the stream ends, it is possible to add an extra For cases where there is a need to emit elements based on the state when the stream ends, it is possible to add an extra
element signalling the end of the stream before the `statefulMapConcat` operator. element signalling the end of the stream before the `statefulMapConcat` operator.

View file

@ -43,40 +43,39 @@ public class StatefulMapConcat {
// #zip-with-index // #zip-with-index
} }
static void blacklist() { static void denylist() {
// #blacklist // #denylist
Source<String, NotUsed> fruitsAndBlacklistCommands = Source<String, NotUsed> fruitsAndDenyCommands =
Source.from( Source.from(
Arrays.asList( Arrays.asList("banana", "pear", "orange", "deny:banana", "banana", "pear", "banana"));
"banana", "pear", "orange", "blacklist:banana", "banana", "pear", "banana"));
Flow<String, String, NotUsed> blacklistingFlow = Flow<String, String, NotUsed> denyFilterFlow =
Flow.of(String.class) Flow.of(String.class)
.statefulMapConcat( .statefulMapConcat(
() -> { () -> {
Set<String> blacklist = new HashSet<>(); Set<String> denyList = new HashSet<>();
return (element) -> { return (element) -> {
if (element.startsWith("blacklist:")) { if (element.startsWith("deny:")) {
blacklist.add(element.substring(10)); denyList.add(element.substring(10));
return Collections return Collections
.emptyList(); // no element downstream when adding a blacklisted keyword .emptyList(); // no element downstream when adding a deny listed keyword
} else if (blacklist.contains(element)) { } else if (denyList.contains(element)) {
return Collections return Collections
.emptyList(); // no element downstream if element is blacklisted .emptyList(); // no element downstream if element is deny listed
} else { } else {
return Collections.singletonList(element); return Collections.singletonList(element);
} }
}; };
}); });
fruitsAndBlacklistCommands.via(blacklistingFlow).runForeach(System.out::println, system); fruitsAndDenyCommands.via(denyFilterFlow).runForeach(System.out::println, system);
// prints // prints
// banana // banana
// pear // pear
// orange // orange
// pear // pear
// #blacklist // #denylist
} }
static void reactOnEnd() { static void reactOnEnd() {

View file

@ -34,33 +34,33 @@ class StatefulMapConcat {
// #zip-with-index // #zip-with-index
} }
def blacklist(): Unit = { def denylist(): Unit = {
// #blacklist // #denylist
val fruitsAndBlacklistCommands = Source( val fruitsAndDeniedCommands = Source(
"banana" :: "pear" :: "orange" :: "blacklist:banana" :: "banana" :: "pear" :: "banana" :: Nil) "banana" :: "pear" :: "orange" :: "deny:banana" :: "banana" :: "pear" :: "banana" :: Nil)
val blacklistingFlow = Flow[String].statefulMapConcat { () => val denyFilterFlow = Flow[String].statefulMapConcat { () =>
var blacklist = Set.empty[String] var denyList = Set.empty[String]
{ element => { element =>
if (element.startsWith("blacklist:")) { if (element.startsWith("deny:")) {
blacklist += element.drop(10) denyList += element.drop(10)
Nil // no element downstream when adding a blacklisted keyword Nil // no element downstream when adding a deny listed keyword
} else if (blacklist(element)) { } else if (denyList(element)) {
Nil // no element downstream if element is blacklisted Nil // no element downstream if element is deny listed
} else { } else {
element :: Nil element :: Nil
} }
} }
} }
fruitsAndBlacklistCommands.via(blacklistingFlow).runForeach(println) fruitsAndDeniedCommands.via(denyFilterFlow).runForeach(println)
// prints // prints
// banana // banana
// pear // pear
// orange // orange
// pear // pear
// #blacklist // #denylist
} }
def reactOnEnd(): Unit = { def reactOnEnd(): Unit = {

View file

@ -0,0 +1,2 @@
# Internal class rename
ProblemFilters.exclude[MissingClassProblem]("akka.remote.NotWhitelistedClassRemoteDeploymentAttemptException")

View file

@ -234,10 +234,18 @@ akka {
# remote deployment configuration section # remote deployment configuration section
deployment { deployment {
# If true, will only allow specific classes to be instanciated on this system via remote deployment # deprecated, use `enable-allow-list`
enable-whitelist = off enable-whitelist = off
# If true, will only allow specific classes listed in `allowed-actor-classes` to be instanciated on this
# system via remote deployment
enable-allow-list = ${akka.remote.deployment.enable-whitelist}
# deprecated, use `allowed-actor-classes`
whitelist = [] whitelist = []
allowed-actor-classes = ${akka.remote.deployment.whitelist}
} }
### Default dispatcher for the remoting subsystem ### Default dispatcher for the remoting subsystem
@ -336,7 +344,7 @@ akka {
untrusted-mode = off untrusted-mode = off
# When 'untrusted-mode=on' inbound actor selections are by default discarded. # When 'untrusted-mode=on' inbound actor selections are by default discarded.
# Actors with paths defined in this white list are granted permission to receive actor # Actors with paths defined in this list are granted permission to receive actor
# selections messages. # selections messages.
# E.g. trusted-selection-paths = ["/user/receptionist", "/user/namingService"] # E.g. trusted-selection-paths = ["/user/receptionist", "/user/namingService"]
trusted-selection-paths = [] trusted-selection-paths = []
@ -807,7 +815,7 @@ akka {
untrusted-mode = off untrusted-mode = off
# When 'untrusted-mode=on' inbound actor selections are by default discarded. # When 'untrusted-mode=on' inbound actor selections are by default discarded.
# Actors with paths defined in this white list are granted permission to receive actor # Actors with paths defined in this list are granted permission to receive actor
# selections messages. # selections messages.
# E.g. trusted-selection-paths = ["/user/receptionist", "/user/namingService"] # E.g. trusted-selection-paths = ["/user/receptionist", "/user/namingService"]
trusted-selection-paths = [] trusted-selection-paths = []

View file

@ -73,10 +73,11 @@ private[akka] class RemoteSystemDaemon(
private val parent2children = new ConcurrentHashMap[ActorRef, Set[ActorRef]] private val parent2children = new ConcurrentHashMap[ActorRef, Set[ActorRef]]
private val whitelistEnabled = system.settings.config.getBoolean("akka.remote.deployment.enable-whitelist") private val allowListEnabled = system.settings.config.getBoolean("akka.remote.deployment.enable-allow-list")
private val remoteDeploymentWhitelist: immutable.Set[String] = { private val remoteDeploymentAllowList: immutable.Set[String] = {
import akka.util.ccompat.JavaConverters._ import akka.util.ccompat.JavaConverters._
if (whitelistEnabled) system.settings.config.getStringList("akka.remote.deployment.whitelist").asScala.toSet if (allowListEnabled)
system.settings.config.getStringList("akka.remote.deployment.allowed-actor-classes").asScala.toSet
else Set.empty else Set.empty
} }
@ -164,13 +165,13 @@ private[akka] class RemoteSystemDaemon(
case DaemonMsgCreate(_, _, path, _) if untrustedMode => case DaemonMsgCreate(_, _, path, _) if untrustedMode =>
log.debug("does not accept deployments (untrusted) for [{}]", path) // TODO add security marker? log.debug("does not accept deployments (untrusted) for [{}]", path) // TODO add security marker?
case DaemonMsgCreate(props, deploy, path, supervisor) if whitelistEnabled => case DaemonMsgCreate(props, deploy, path, supervisor) if allowListEnabled =>
val name = props.clazz.getCanonicalName val name = props.clazz.getCanonicalName
if (remoteDeploymentWhitelist.contains(name)) if (remoteDeploymentAllowList.contains(name))
doCreateActor(message, props, deploy, path, supervisor) doCreateActor(message, props, deploy, path, supervisor)
else { else {
val ex = val ex =
new NotWhitelistedClassRemoteDeploymentAttemptException(props.actorClass(), remoteDeploymentWhitelist) new NotAllowedClassRemoteDeploymentAttemptException(props.actorClass(), remoteDeploymentAllowList)
log.error( log.error(
LogMarker.Security, LogMarker.Security,
ex, ex,
@ -273,8 +274,8 @@ private[akka] class RemoteSystemDaemon(
} }
/** INTERNAL API */ /** INTERNAL API */
final class NotWhitelistedClassRemoteDeploymentAttemptException(illegal: Class[_], whitelist: immutable.Set[String]) final class NotAllowedClassRemoteDeploymentAttemptException(illegal: Class[_], allowedClassNames: immutable.Set[String])
extends RuntimeException( extends RuntimeException(
s"Attempted to deploy not whitelisted Actor class: " + s"Attempted to deploy Actor class: " +
s"[$illegal], " + s"[$illegal], " +
s"whitelisted classes: [${whitelist.mkString(", ")}]") s"which is not allowed, allowed classes: [${allowedClassNames.mkString(", ")}]")

View file

@ -47,9 +47,9 @@ class ProtobufSerializer(val system: ExtendedActorSystem) extends BaseSerializer
private val parsingMethodBindingRef = new AtomicReference[Map[Class[_], Method]](Map.empty) private val parsingMethodBindingRef = new AtomicReference[Map[Class[_], Method]](Map.empty)
private val toByteArrayMethodBindingRef = new AtomicReference[Map[Class[_], Method]](Map.empty) private val toByteArrayMethodBindingRef = new AtomicReference[Map[Class[_], Method]](Map.empty)
private val whitelistClassNames: Set[String] = { private val allowedClassNames: Set[String] = {
import akka.util.ccompat.JavaConverters._ import akka.util.ccompat.JavaConverters._
system.settings.config.getStringList("akka.serialization.protobuf.whitelist-class").asScala.toSet system.settings.config.getStringList("akka.serialization.protobuf.allowed-classes").asScala.toSet
} }
// This must lazy otherwise it will deadlock the ActorSystem creation // This must lazy otherwise it will deadlock the ActorSystem creation
@ -110,18 +110,18 @@ class ProtobufSerializer(val system: ExtendedActorSystem) extends BaseSerializer
} }
private def checkAllowedClass(clazz: Class[_]): Unit = { private def checkAllowedClass(clazz: Class[_]): Unit = {
if (!isInWhitelist(clazz)) { if (!isInAllowList(clazz)) {
val warnMsg = s"Can't deserialize object of type [${clazz.getName}] in [${getClass.getName}]. " + val warnMsg = s"Can't deserialize object of type [${clazz.getName}] in [${getClass.getName}]. " +
"Only classes that are whitelisted are allowed for security reasons. " + "Only classes that are on the allow list are allowed for security reasons. " +
"Configure whitelist with akka.actor.serialization-bindings or " + "Configure allowed classes with akka.actor.serialization-bindings or " +
"akka.serialization.protobuf.whitelist-class" "akka.serialization.protobuf.allowed-classes"
log.warning(LogMarker.Security, warnMsg) log.warning(LogMarker.Security, warnMsg)
throw new IllegalArgumentException(warnMsg) throw new IllegalArgumentException(warnMsg)
} }
} }
/** /**
* Using the `serialization-bindings` as source for the whitelist. * Using the `serialization-bindings` as source for the allowed classes.
* Note that the intended usage of serialization-bindings is for lookup of * Note that the intended usage of serialization-bindings is for lookup of
* serializer when serializing (`toBinary`). For deserialization (`fromBinary`) the serializer-id is * serializer when serializing (`toBinary`). For deserialization (`fromBinary`) the serializer-id is
* used for selecting serializer. * used for selecting serializer.
@ -130,13 +130,13 @@ class ProtobufSerializer(val system: ExtendedActorSystem) extends BaseSerializer
* *
* If an old class is removed from `serialization-bindings` when it's not used for serialization * If an old class is removed from `serialization-bindings` when it's not used for serialization
* but still used for deserialization (e.g. rolling update with serialization changes) it can * but still used for deserialization (e.g. rolling update with serialization changes) it can
* be allowed by specifying in `akka.protobuf.whitelist-class`. * be allowed by specifying in `akka.protobuf.allowed-classes`.
* *
* That is also possible when changing a binding from a ProtobufSerializer to another serializer (e.g. Jackson) * That is also possible when changing a binding from a ProtobufSerializer to another serializer (e.g. Jackson)
* and still bind with the same class (interface). * and still bind with the same class (interface).
*/ */
private def isInWhitelist(clazz: Class[_]): Boolean = { private def isInAllowList(clazz: Class[_]): Boolean = {
isBoundToProtobufSerializer(clazz) || isInWhitelistClassName(clazz) isBoundToProtobufSerializer(clazz) || isInAllowListClassName(clazz)
} }
private def isBoundToProtobufSerializer(clazz: Class[_]): Boolean = { private def isBoundToProtobufSerializer(clazz: Class[_]): Boolean = {
@ -148,9 +148,9 @@ class ProtobufSerializer(val system: ExtendedActorSystem) extends BaseSerializer
} }
} }
private def isInWhitelistClassName(clazz: Class[_]): Boolean = { private def isInAllowListClassName(clazz: Class[_]): Boolean = {
whitelistClassNames(clazz.getName) || allowedClassNames(clazz.getName) ||
whitelistClassNames(clazz.getSuperclass.getName) || allowedClassNames(clazz.getSuperclass.getName) ||
clazz.getInterfaces.exists(c => whitelistClassNames(c.getName)) clazz.getInterfaces.exists(c => allowedClassNames(c.getName))
} }
} }

View file

@ -94,7 +94,7 @@ class UntrustedSpec extends ArteryMultiNodeSpec(UntrustedSpec.config) with Impli
"UntrustedMode" must { "UntrustedMode" must {
"allow actor selection to configured white list" in { "allow actor selection to configured allow list" in {
val sel = client.actorSelection(RootActorPath(address) / receptionist.path.elements) val sel = client.actorSelection(RootActorPath(address) / receptionist.path.elements)
sel ! "hello" sel ! "hello"
expectMsg("hello") expectMsg("hello")
@ -152,7 +152,7 @@ class UntrustedSpec extends ArteryMultiNodeSpec(UntrustedSpec.config) with Impli
expectNoMessage(1.second) expectNoMessage(1.second)
} }
"discard actor selection to child of matching white list" in { "discard actor selection to child of matching allow list" in {
val sel = client.actorSelection(RootActorPath(address) / receptionist.path.elements / "child1") val sel = client.actorSelection(RootActorPath(address) / receptionist.path.elements / "child1")
sel ! "hello" sel ! "hello"
expectNoMessage(1.second) expectNoMessage(1.second)

View file

@ -5,19 +5,18 @@
package akka.remote.classic package akka.remote.classic
import scala.concurrent.duration._ import scala.concurrent.duration._
import com.github.ghik.silencer.silent import com.github.ghik.silencer.silent
import com.typesafe.config._ import com.typesafe.config._
import akka.actor._ import akka.actor._
import akka.remote.EndpointException import akka.remote.EndpointException
import akka.remote.NotAllowedClassRemoteDeploymentAttemptException
import akka.remote.transport._ import akka.remote.transport._
import akka.testkit._ import akka.testkit._
// relies on test transport // relies on test transport
object RemoteDeploymentWhitelistSpec { object RemoteDeploymentAllowListSpec {
class EchoWhitelisted extends Actor { class EchoAllowed extends Actor {
var target: ActorRef = context.system.deadLetters var target: ActorRef = context.system.deadLetters
def receive = { def receive = {
@ -35,7 +34,7 @@ object RemoteDeploymentWhitelistSpec {
} }
} }
class EchoNotWhitelisted extends Actor { class EchoNotAllowed extends Actor {
var target: ActorRef = context.system.deadLetters var target: ActorRef = context.system.deadLetters
def receive = { def receive = {
@ -57,6 +56,7 @@ object RemoteDeploymentWhitelistSpec {
akka { akka {
actor.provider = remote actor.provider = remote
remote { remote {
use-unsafe-remote-features-outside-cluster = on use-unsafe-remote-features-outside-cluster = on
classic.enabled-transports = [ classic.enabled-transports = [
@ -79,7 +79,7 @@ object RemoteDeploymentWhitelistSpec {
transport-class = "akka.remote.transport.TestTransport" transport-class = "akka.remote.transport.TestTransport"
applied-adapters = [] applied-adapters = []
registry-key = aX33k0jWKg registry-key = aX33k0jWKg
local-address = "test://RemoteDeploymentWhitelistSpec@localhost:12345" local-address = "test://RemoteDeploymentAllowListSpec@localhost:12345"
maximum-payload-bytes = 32000 bytes maximum-payload-bytes = 32000 bytes
scheme-identifier = test scheme-identifier = test
} }
@ -105,31 +105,30 @@ object RemoteDeploymentWhitelistSpec {
} }
@silent("deprecated") @silent("deprecated")
class RemoteDeploymentWhitelistSpec class RemoteDeploymentAllowListSpec
extends AkkaSpec(RemoteDeploymentWhitelistSpec.cfg) extends AkkaSpec(RemoteDeploymentAllowListSpec.cfg)
with ImplicitSender with ImplicitSender
with DefaultTimeout { with DefaultTimeout {
import RemoteDeploymentWhitelistSpec._ import RemoteDeploymentAllowListSpec._
val conf = val conf =
ConfigFactory.parseString(""" ConfigFactory.parseString("""
akka.loglevel = DEBUG
akka.remote.test { akka.remote.test {
local-address = "test://remote-sys@localhost:12346" local-address = "test://remote-sys@localhost:12346"
maximum-payload-bytes = 48000 bytes maximum-payload-bytes = 48000 bytes
} }
//#whitelist-config //#allow-list-config
akka.remote.deployment { akka.remote.deployment {
enable-whitelist = on enable-allow-list = on
whitelist = [ allowed-actor-classes = [
"NOT_ON_CLASSPATH", # verify we don't throw if a class not on classpath is listed here "NOT_ON_CLASSPATH", # verify we don't throw if a class not on classpath is listed here
"akka.remote.classic.RemoteDeploymentWhitelistSpec.EchoWhitelisted" "akka.remote.classic.RemoteDeploymentAllowListSpec.EchoAllowed"
] ]
} }
//#whitelist-config //#allow-list-config
""").withFallback(system.settings.config).resolve() """).withFallback(system.settings.config).resolve()
val remoteSystem = ActorSystem("remote-sys", conf) val remoteSystem = ActorSystem("remote-sys", conf)
@ -147,10 +146,10 @@ class RemoteDeploymentWhitelistSpec
AssociationRegistry.clear() AssociationRegistry.clear()
} }
"RemoteDeployment Whitelist" must { "RemoteDeployment Allow List" must {
"allow deploying Echo actor (included in whitelist)" in { "allow deploying Echo actor (included in allow list)" in {
val r = system.actorOf(Props[EchoWhitelisted](), "blub") val r = system.actorOf(Props[EchoAllowed](), "blub")
r.path.toString should ===( r.path.toString should ===(
s"akka.test://remote-sys@localhost:12346/remote/akka.test/${getClass.getSimpleName}@localhost:12345/user/blub") s"akka.test://remote-sys@localhost:12346/remote/akka.test/${getClass.getSimpleName}@localhost:12345/user/blub")
r ! 42 r ! 42
@ -165,13 +164,19 @@ class RemoteDeploymentWhitelistSpec
expectMsg("postStop") expectMsg("postStop")
} }
"not deploy actor not listed in whitelist" in { "not deploy actor not listed in allow list" in {
val r = system.actorOf(Props[EchoNotWhitelisted](), "danger-mouse") EventFilter
.warning(start = "received dead letter", occurrences = 1)
.intercept {
EventFilter[NotAllowedClassRemoteDeploymentAttemptException](occurrences = 1).intercept {
val r = system.actorOf(Props[EchoNotAllowed](), "danger-mouse")
r.path.toString should ===( r.path.toString should ===(
s"akka.test://remote-sys@localhost:12346/remote/akka.test/${getClass.getSimpleName}@localhost:12345/user/danger-mouse") s"akka.test://remote-sys@localhost:12346/remote/akka.test/${getClass.getSimpleName}@localhost:12345/user/danger-mouse")
r ! 42 r ! 42
expectNoMessage(1.second) expectNoMessage(1.second)
system.stop(r) system.stop(r)
}(remoteSystem)
}(remoteSystem)
} }
} }
} }

View file

@ -123,7 +123,7 @@ akka.actor.serialization-bindings {
"UntrustedMode" must { "UntrustedMode" must {
"allow actor selection to configured white list" in { "allow actor selection to configured allow list" in {
val sel = client.actorSelection(RootActorPath(address) / receptionist.path.elements) val sel = client.actorSelection(RootActorPath(address) / receptionist.path.elements)
sel ! "hello" sel ! "hello"
expectMsg("hello") expectMsg("hello")
@ -181,7 +181,7 @@ akka.actor.serialization-bindings {
expectNoMessage(1.second) expectNoMessage(1.second)
} }
"discard actor selection to child of matching white list" in { "discard actor selection to child of matching allow list" in {
val sel = client.actorSelection(RootActorPath(address) / receptionist.path.elements / "child1") val sel = client.actorSelection(RootActorPath(address) / receptionist.path.elements / "child1")
sel ! "hello" sel ! "hello"
expectNoMessage(1.second) expectNoMessage(1.second)

View file

@ -45,7 +45,7 @@ object MaliciousMessage {
} }
class ProtobufSerializerSpec extends AkkaSpec(s""" class ProtobufSerializerSpec extends AkkaSpec(s"""
akka.serialization.protobuf.whitelist-class = [ akka.serialization.protobuf.allowed-classes = [
"com.google.protobuf.GeneratedMessage", "com.google.protobuf.GeneratedMessage",
"com.google.protobuf.GeneratedMessageV3", "com.google.protobuf.GeneratedMessageV3",
"scalapb.GeneratedMessageCompanion", "scalapb.GeneratedMessageCompanion",
@ -87,7 +87,7 @@ class ProtobufSerializerSpec extends AkkaSpec(s"""
protobufV3Message should ===(deserialized) protobufV3Message should ===(deserialized)
} }
"disallow deserialization of classes that are not in bindings and not in configured whitelist-class" in { "disallow deserialization of classes that are not in bindings and not in configured allowed classes" in {
val originalSerializer = ser.serializerFor(classOf[MyMessage]) val originalSerializer = ser.serializerFor(classOf[MyMessage])
intercept[IllegalArgumentException] { intercept[IllegalArgumentException] {
@ -95,7 +95,7 @@ class ProtobufSerializerSpec extends AkkaSpec(s"""
} }
} }
"allow deserialization of classes in configured whitelist-class" in { "allow deserialization of classes in configured allowed classes" in {
val originalSerializer = ser.serializerFor(classOf[MyMessage]) val originalSerializer = ser.serializerFor(classOf[MyMessage])
val deserialized = val deserialized =
@ -103,7 +103,7 @@ class ProtobufSerializerSpec extends AkkaSpec(s"""
deserialized.getClass should ===(classOf[AnotherMessage]) deserialized.getClass should ===(classOf[AnotherMessage])
} }
"allow deserialization of interfaces in configured whitelist-class" in { "allow deserialization of interfaces in configured allowed classes" in {
val originalSerializer = ser.serializerFor(classOf[MyMessage]) val originalSerializer = ser.serializerFor(classOf[MyMessage])
val deserialized = val deserialized =
@ -111,7 +111,7 @@ class ProtobufSerializerSpec extends AkkaSpec(s"""
deserialized.getClass should ===(classOf[AnotherMessage2]) deserialized.getClass should ===(classOf[AnotherMessage2])
} }
"allow deserialization of super classes in configured whitelist-class" in { "allow deserialization of super classes in configured allowed classes" in {
val originalSerializer = ser.serializerFor(classOf[MyMessage]) val originalSerializer = ser.serializerFor(classOf[MyMessage])
val deserialized = val deserialized =

View file

@ -0,0 +1,2 @@
# Internal class rename
ProblemFilters.exclude[MissingClassProblem]("akka.serialization.jackson.JacksonSerializer$GadgetClassBlacklist")

View file

@ -127,10 +127,14 @@ akka.serialization.jackson {
# } # }
json-write-features {} json-write-features {}
# Deprecated, use `allowed-class-prefix` instead
whitelist-class-prefix = []
# Additional classes that are allowed even if they are not defined in `serialization-bindings`. # Additional classes that are allowed even if they are not defined in `serialization-bindings`.
# This is useful when a class is not used for serialization any more and therefore removed # This is useful when a class is not used for serialization any more and therefore removed
# from `serialization-bindings`, but should still be possible to deserialize. # from `serialization-bindings`, but should still be possible to deserialize.
whitelist-class-prefix = [] allowed-class-prefix = ${akka.serialization.jackson.whitelist-class-prefix}
# settings for compression of the payload # settings for compression of the payload
compression { compression {

View file

@ -30,11 +30,11 @@ import akka.util.OptionVal
@InternalApi private[akka] object JacksonSerializer { @InternalApi private[akka] object JacksonSerializer {
/** /**
* Using the blacklist from Jackson databind of class names that shouldn't be allowed. * Using the deny list from Jackson databind of class names that shouldn't be allowed.
* Not nice to depend on implementation details of Jackson, but good to use the same * Not nice to depend on implementation details of Jackson, but good to use the same
* list to automatically have the list updated when new classes are added in Jackson. * list to automatically have the list updated when new classes are added in Jackson.
*/ */
class GadgetClassBlacklist extends SubTypeValidator { class GadgetClassDenyList extends SubTypeValidator {
private def defaultNoDeserClassNames: java.util.Set[String] = private def defaultNoDeserClassNames: java.util.Set[String] =
SubTypeValidator.DEFAULT_NO_DESER_CLASS_NAMES // it's has protected visibility SubTypeValidator.DEFAULT_NO_DESER_CLASS_NAMES // it's has protected visibility
@ -204,10 +204,10 @@ import akka.util.OptionVal
k -> transformer k -> transformer
} }
} }
private val blacklist: GadgetClassBlacklist = new GadgetClassBlacklist private val denyList: GadgetClassDenyList = new GadgetClassDenyList
private val whitelistClassPrefix = { private val allowedClassPrefix = {
import akka.util.ccompat.JavaConverters._ import akka.util.ccompat.JavaConverters._
conf.getStringList("whitelist-class-prefix").asScala.toVector conf.getStringList("allowed-class-prefix").asScala.toVector
} }
private val typeInManifest: Boolean = conf.getBoolean("type-in-manifest") private val typeInManifest: Boolean = conf.getBoolean("type-in-manifest")
// Calculated eagerly so as to fail fast // Calculated eagerly so as to fail fast
@ -397,32 +397,32 @@ import akka.util.OptionVal
className.length > 0 && className.charAt(className.length - 1) == '$' className.length > 0 && className.charAt(className.length - 1) == '$'
private def checkAllowedClassName(className: String): Unit = { private def checkAllowedClassName(className: String): Unit = {
if (!blacklist.isAllowedClassName(className)) { if (!denyList.isAllowedClassName(className)) {
val warnMsg = s"Can't serialize/deserialize object of type [$className] in [${getClass.getName}]. " + val warnMsg = s"Can't serialize/deserialize object of type [$className] in [${getClass.getName}]. " +
s"Blacklisted for security reasons." s"Disallowed (on deny list) for security reasons."
log.warning(LogMarker.Security, warnMsg) log.warning(LogMarker.Security, warnMsg)
throw new IllegalArgumentException(warnMsg) throw new IllegalArgumentException(warnMsg)
} }
} }
private def checkAllowedClass(clazz: Class[_]): Unit = { private def checkAllowedClass(clazz: Class[_]): Unit = {
if (!blacklist.isAllowedClass(clazz)) { if (!denyList.isAllowedClass(clazz)) {
val warnMsg = s"Can't serialize/deserialize object of type [${clazz.getName}] in [${getClass.getName}]. " + val warnMsg = s"Can't serialize/deserialize object of type [${clazz.getName}] in [${getClass.getName}]. " +
s"Blacklisted for security reasons." s"Not allowed for security reasons."
log.warning(LogMarker.Security, warnMsg) log.warning(LogMarker.Security, warnMsg)
throw new IllegalArgumentException(warnMsg) throw new IllegalArgumentException(warnMsg)
} else if (!isInWhitelist(clazz)) { } else if (!isInAllowList(clazz)) {
val warnMsg = s"Can't serialize/deserialize object of type [${clazz.getName}] in [${getClass.getName}]. " + val warnMsg = s"Can't serialize/deserialize object of type [${clazz.getName}] in [${getClass.getName}]. " +
"Only classes that are whitelisted are allowed for security reasons. " + "Only classes that are listed as allowed are allowed for security reasons. " +
"Configure whitelist with akka.actor.serialization-bindings or " + "Configure allowed classes with akka.actor.serialization-bindings or " +
"akka.serialization.jackson.whitelist-class-prefix." "akka.serialization.jackson.allowed-class-prefix."
log.warning(LogMarker.Security, warnMsg) log.warning(LogMarker.Security, warnMsg)
throw new IllegalArgumentException(warnMsg) throw new IllegalArgumentException(warnMsg)
} }
} }
/** /**
* Using the `serialization-bindings` as source for the whitelist. * Using the `serialization-bindings` as source for the allowed classes.
* Note that the intended usage of serialization-bindings is for lookup of * Note that the intended usage of serialization-bindings is for lookup of
* serializer when serializing (`toBinary`). For deserialization (`fromBinary`) the serializer-id is * serializer when serializing (`toBinary`). For deserialization (`fromBinary`) the serializer-id is
* used for selecting serializer. * used for selecting serializer.
@ -431,13 +431,13 @@ import akka.util.OptionVal
* *
* If an old class is removed from `serialization-bindings` when it's not used for serialization * If an old class is removed from `serialization-bindings` when it's not used for serialization
* but still used for deserialization (e.g. rolling update with serialization changes) it can * but still used for deserialization (e.g. rolling update with serialization changes) it can
* be allowed by specifying in `whitelist-class-prefix`. * be allowed by specifying in `allowed-class-prefix`.
* *
* That is also possible when changing a binding from a JacksonSerializer to another serializer (e.g. protobuf) * That is also possible when changing a binding from a JacksonSerializer to another serializer (e.g. protobuf)
* and still bind with the same class (interface). * and still bind with the same class (interface).
*/ */
private def isInWhitelist(clazz: Class[_]): Boolean = { private def isInAllowList(clazz: Class[_]): Boolean = {
isBoundToJacksonSerializer(clazz) || isInWhitelistClassPrefix(clazz.getName) isBoundToJacksonSerializer(clazz) || hasAllowedClassPrefix(clazz.getName)
} }
private def isBoundToJacksonSerializer(clazz: Class[_]): Boolean = { private def isBoundToJacksonSerializer(clazz: Class[_]): Boolean = {
@ -452,8 +452,8 @@ import akka.util.OptionVal
} }
} }
private def isInWhitelistClassPrefix(className: String): Boolean = private def hasAllowedClassPrefix(className: String): Boolean =
whitelistClassPrefix.exists(className.startsWith) allowedClassPrefix.exists(className.startsWith)
/** /**
* Check that serialization-bindings are not configured with open-ended interfaces, * Check that serialization-bindings are not configured with open-ended interfaces,

View file

@ -471,7 +471,7 @@ class JacksonJsonSerializerSpec extends JacksonSerializerSpec("jackson-json") {
} }
} }
"allow deserialization of classes in configured whitelist-class-prefix" in { "allow deserialization of classes in configured allowed-class-prefix" in {
val json = """{"name":"abc"}""" val json = """{"name":"abc"}"""
val old = SimpleCommand("abc") val old = SimpleCommand("abc")
@ -638,7 +638,7 @@ abstract class JacksonSerializerSpec(serializerName: String)
"akka.serialization.jackson.JavaTestMessages$$TestMessage" = $serializerName "akka.serialization.jackson.JavaTestMessages$$TestMessage" = $serializerName
} }
} }
akka.serialization.jackson.whitelist-class-prefix = ["akka.serialization.jackson.ScalaTestMessages$$OldCommand"] akka.serialization.jackson.allowed-class-prefix = ["akka.serialization.jackson.ScalaTestMessages$$OldCommand"]
"""))) """)))
with AnyWordSpecLike with AnyWordSpecLike
with Matchers with Matchers
@ -900,17 +900,17 @@ abstract class JacksonSerializerSpec(serializerName: String)
event2.field2 should ===(17) event2.field2 should ===(17)
} }
"not allow serialization of blacklisted class" in { "not allow serialization of deny listed class" in {
val serializer = serializerFor(SimpleCommand("ok")) val serializer = serializerFor(SimpleCommand("ok"))
val fileHandler = new FileHandler(s"target/tmp-${this.getClass.getName}") val fileHandler = new FileHandler(s"target/tmp-${this.getClass.getName}")
try { try {
intercept[IllegalArgumentException] { intercept[IllegalArgumentException] {
serializer.manifest(fileHandler) serializer.manifest(fileHandler)
}.getMessage.toLowerCase should include("blacklist") }.getMessage.toLowerCase should include("deny list")
} finally fileHandler.close() } finally fileHandler.close()
} }
"not allow deserialization of blacklisted class" in { "not allow deserialization of deny list class" in {
withTransportInformation() { () => withTransportInformation() { () =>
val msg = SimpleCommand("ok") val msg = SimpleCommand("ok")
val serializer = serializerFor(msg) val serializer = serializerFor(msg)
@ -918,18 +918,18 @@ abstract class JacksonSerializerSpec(serializerName: String)
intercept[IllegalArgumentException] { intercept[IllegalArgumentException] {
// maliciously changing manifest // maliciously changing manifest
serializer.fromBinary(blob, classOf[FileHandler].getName) serializer.fromBinary(blob, classOf[FileHandler].getName)
}.getMessage.toLowerCase should include("blacklist") }.getMessage.toLowerCase should include("deny list")
} }
} }
"not allow serialization of class that is not in serialization-bindings (whitelist)" in { "not allow serialization of class that is not in serialization-bindings (allowed-class-prefix)" in {
val serializer = serializerFor(SimpleCommand("ok")) val serializer = serializerFor(SimpleCommand("ok"))
intercept[IllegalArgumentException] { intercept[IllegalArgumentException] {
serializer.manifest(Status.Success("bad")) serializer.manifest(Status.Success("bad"))
}.getMessage.toLowerCase should include("whitelist") }.getMessage.toLowerCase should include("allowed-class-prefix")
} }
"not allow deserialization of class that is not in serialization-bindings (whitelist)" in { "not allow deserialization of class that is not in serialization-bindings (allowed-class-prefix)" in {
withTransportInformation() { () => withTransportInformation() { () =>
val msg = SimpleCommand("ok") val msg = SimpleCommand("ok")
val serializer = serializerFor(msg) val serializer = serializerFor(msg)
@ -937,7 +937,7 @@ abstract class JacksonSerializerSpec(serializerName: String)
intercept[IllegalArgumentException] { intercept[IllegalArgumentException] {
// maliciously changing manifest // maliciously changing manifest
serializer.fromBinary(blob, classOf[Status.Success].getName) serializer.fromBinary(blob, classOf[Status.Success].getName)
}.getMessage.toLowerCase should include("whitelist") }.getMessage.toLowerCase should include("allowed-class-prefix")
} }
} }

View file

@ -150,11 +150,11 @@ object SerializationDocSpec {
#//#date-time #//#date-time
""" """
val configWhitelist = """ val configAllowList = """
#//#whitelist-class-prefix #//#allowed-class-prefix
akka.serialization.jackson.whitelist-class-prefix = akka.serialization.jackson.allowed-class-prefix =
["com.myservice.event.OrderAdded", "com.myservice.command"] ["com.myservice.event.OrderAdded", "com.myservice.command"]
#//#whitelist-class-prefix #//#allowed-class-prefix
""" """
} }