diff --git a/akka-docs/rst/java/remoting.rst b/akka-docs/rst/java/remoting.rst index df963a9fdc..f9535cb6c2 100644 --- a/akka-docs/rst/java/remoting.rst +++ b/akka-docs/rst/java/remoting.rst @@ -479,6 +479,14 @@ a denial of service attack). :class:`PossiblyHarmful` covers the predefined messages like :class:`PoisonPill` and :class:`Kill`, but it can also be added as a marker trait to user-defined messages. +Messages sent with actor selection are by default discarded in untrusted mode, but +permission to receive actor selection messages can be granted to specific actors +defined in configuration:: + + akka.remote.trusted-selection-paths = ["/user/receptionist", "/user/namingService"] + +The actual message must still not be of type :class:`PossiblyHarmful`. + In summary, the following operations are ignored by a system configured in untrusted mode when incoming via the remoting layer: @@ -487,6 +495,7 @@ untrusted mode when incoming via the remoting layer: * ``system.stop()``, :class:`PoisonPill`, :class:`Kill` * sending any message which extends from the :class:`PossiblyHarmful` marker interface, which includes :class:`Terminated` +* messages sent with actor selection, unless destination defined in ``trusted-selection-paths``. .. note:: diff --git a/akka-docs/rst/scala/remoting.rst b/akka-docs/rst/scala/remoting.rst index 3e8a9db1d4..367ac1d0df 100644 --- a/akka-docs/rst/scala/remoting.rst +++ b/akka-docs/rst/scala/remoting.rst @@ -480,6 +480,14 @@ a denial of service attack). :class:`PossiblyHarmful` covers the predefined messages like :class:`PoisonPill` and :class:`Kill`, but it can also be added as a marker trait to user-defined messages. +Messages sent with actor selection are by default discarded in untrusted mode, but +permission to receive actor selection messages can be granted to specific actors +defined in configuration:: + + akka.remote.trusted-selection-paths = ["/user/receptionist", "/user/namingService"] + +The actual message must still not be of type :class:`PossiblyHarmful`. + In summary, the following operations are ignored by a system configured in untrusted mode when incoming via the remoting layer: @@ -488,6 +496,7 @@ untrusted mode when incoming via the remoting layer: * ``system.stop()``, :class:`PoisonPill`, :class:`Kill` * sending any message which extends from the :class:`PossiblyHarmful` marker interface, which includes :class:`Terminated` +* messages sent with actor selection, unless destination defined in ``trusted-selection-paths``. .. note:: diff --git a/akka-remote/src/main/resources/reference.conf b/akka-remote/src/main/resources/reference.conf index 22387f0b1f..523b875b67 100644 --- a/akka-remote/src/main/resources/reference.conf +++ b/akka-remote/src/main/resources/reference.conf @@ -98,6 +98,12 @@ akka { # system messages to be send by clients, e.g. messages like 'Create', # 'Suspend', 'Resume', 'Terminate', 'Supervise', 'Link' etc. untrusted-mode = off + + # 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 + # selections messages. + # E.g. trusted-selection-paths = ["/user/receptionist", "/user/namingService"] + trusted-selection-paths = [] # Should the remote server require that its peers share the same # secure-cookie (defined in the 'remote' section)? Secure cookies are passed diff --git a/akka-remote/src/main/scala/akka/remote/Endpoint.scala b/akka-remote/src/main/scala/akka/remote/Endpoint.scala index 89c52ea35a..feeca30f96 100644 --- a/akka-remote/src/main/scala/akka/remote/Endpoint.scala +++ b/akka-remote/src/main/scala/akka/remote/Endpoint.scala @@ -71,13 +71,19 @@ private[remote] class DefaultMessageDispatcher(private val system: ExtendedActor case l @ (_: LocalRef | _: RepointableRef) if l.isLocal ⇒ if (LogReceive) log.debug("received local message {}", msgLog) payload match { - case msg: PossiblyHarmful if UntrustedMode ⇒ - log.debug("operating in UntrustedMode, dropping inbound PossiblyHarmful message of type {}", msg.getClass) - case msg: SystemMessage ⇒ l.sendSystemMessage(msg) case sel: ActorSelectionMessage ⇒ - // run the receive logic for ActorSelectionMessage here to make sure it is not stuck on busy user actor - ActorSelection.deliverSelection(l, sender, sel) - case msg ⇒ l.!(msg)(sender) + if (UntrustedMode && (!TrustedSelectionPaths.contains(sel.elements.mkString("/", "/", "")) || + sel.msg.isInstanceOf[PossiblyHarmful] || l != provider.rootGuardian)) + log.debug("operating in UntrustedMode, dropping inbound actor selection to [{}], " + + "allow it by adding the path to 'akka.remote.trusted-selection-paths' configuration", + sel.elements.mkString("/", "/", "")) + else + // run the receive logic for ActorSelectionMessage here to make sure it is not stuck on busy user actor + ActorSelection.deliverSelection(l, sender, sel) + case msg: PossiblyHarmful if UntrustedMode ⇒ + log.debug("operating in UntrustedMode, dropping inbound PossiblyHarmful message of type [{}]", msg.getClass.getName) + case msg: SystemMessage ⇒ l.sendSystemMessage(msg) + case msg ⇒ l.!(msg)(sender) } case r @ (_: RemoteRef | _: RepointableRef) if !r.isLocal && !UntrustedMode ⇒ diff --git a/akka-remote/src/main/scala/akka/remote/RemoteSettings.scala b/akka-remote/src/main/scala/akka/remote/RemoteSettings.scala index 0529c94842..fd5502b1fc 100644 --- a/akka-remote/src/main/scala/akka/remote/RemoteSettings.scala +++ b/akka-remote/src/main/scala/akka/remote/RemoteSettings.scala @@ -25,6 +25,9 @@ final class RemoteSettings(val config: Config) { val UntrustedMode: Boolean = getBoolean("akka.remote.untrusted-mode") + val TrustedSelectionPaths: Set[String] = + immutableSeq(getStringList("akka.remote.trusted-selection-paths")).toSet + val RemoteLifecycleEventsLogLevel: LogLevel = getString("akka.remote.log-remote-lifecycle-events").toLowerCase() match { case "on" ⇒ Logging.DebugLevel case other ⇒ Logging.levelFor(other) match { diff --git a/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala b/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala index 5d905c2470..5240867321 100644 --- a/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/RemoteConfigSpec.scala @@ -27,6 +27,7 @@ class RemoteConfigSpec extends AkkaSpec( LogReceive must be(false) LogSend must be(false) UntrustedMode must be(false) + TrustedSelectionPaths must be(Set.empty[String]) LogRemoteLifecycleEvents must be(true) ShutdownTimeout.duration must be(10 seconds) FlushWait must be(2 seconds) diff --git a/akka-remote/src/test/scala/akka/remote/UntrustedSpec.scala b/akka-remote/src/test/scala/akka/remote/UntrustedSpec.scala index 64a6163608..ed88f4baca 100644 --- a/akka-remote/src/test/scala/akka/remote/UntrustedSpec.scala +++ b/akka-remote/src/test/scala/akka/remote/UntrustedSpec.scala @@ -4,85 +4,178 @@ package akka.remote +import scala.concurrent.duration._ +import com.typesafe.config.ConfigFactory +import akka.actor.Actor +import akka.actor.ActorIdentity +import akka.actor.ActorRef +import akka.actor.ActorSystem +import akka.actor.Deploy +import akka.actor.ExtendedActorSystem +import akka.actor.Identify +import akka.actor.PoisonPill +import akka.actor.Props +import akka.actor.RootActorPath +import akka.actor.Terminated import akka.testkit.AkkaSpec import akka.testkit.ImplicitSender -import akka.actor.ActorSystem -import com.typesafe.config.ConfigFactory -import akka.actor.ExtendedActorSystem -import akka.actor.RootActorPath -import akka.testkit.EventFilter +import akka.testkit.TestProbe +import akka.actor.ActorSelection import akka.testkit.TestEvent -import akka.actor.Props -import akka.actor.Actor import akka.event.Logging -import org.scalatest.junit.JUnitRunner -import org.junit.runner.RunWith -import akka.actor.Terminated -import scala.concurrent.duration._ -import akka.actor.PoisonPill -import akka.actor.Deploy +import akka.testkit.EventFilter + +object UntrustedSpec { + case class IdentifyReq(path: String) + case class StopChild(name: String) + + class Receptionist(testActor: ActorRef) extends Actor { + context.actorOf(Props(classOf[Child], testActor), "child1") + context.actorOf(Props(classOf[Child], testActor), "child2") + context.actorOf(Props(classOf[FakeUser], testActor), "user") + + def receive = { + case IdentifyReq(path) ⇒ context.actorSelection(path).tell(Identify(None), sender) + case StopChild(name) ⇒ context.child(name) foreach context.stop + case msg ⇒ testActor forward msg + } + } + + class Child(testActor: ActorRef) extends Actor { + override def postStop(): Unit = { + testActor ! s"${self.path.name} stopped" + } + def receive = { + case msg ⇒ testActor forward msg + } + } + + class FakeUser(testActor: ActorRef) extends Actor { + context.actorOf(Props(classOf[Child], testActor), "receptionist") + def receive = { + case msg ⇒ testActor forward msg + } + } + +} -@RunWith(classOf[JUnitRunner]) class UntrustedSpec extends AkkaSpec(""" akka.actor.provider = akka.remote.RemoteActorRefProvider akka.remote.untrusted-mode = on +akka.remote.trusted-selection-paths = ["/user/receptionist", ] akka.remote.netty.tcp.port = 0 akka.loglevel = DEBUG """) with ImplicitSender { - val other = ActorSystem("UntrustedSpec-client", ConfigFactory.parseString(""" + import UntrustedSpec._ + + val client = ActorSystem("UntrustedSpec-client", ConfigFactory.parseString(""" akka.actor.provider = akka.remote.RemoteActorRefProvider akka.remote.netty.tcp.port = 0 """)) - val addr = system.asInstanceOf[ExtendedActorSystem].provider.asInstanceOf[RemoteActorRefProvider].transport.addresses.head - val target1 = other.actorFor(RootActorPath(addr) / "remote") - val target2 = other.actorFor(RootActorPath(addr) / testActor.path.elements) + val addr = system.asInstanceOf[ExtendedActorSystem].provider.getDefaultAddress + + val receptionist = system.actorOf(Props(classOf[Receptionist], testActor), "receptionist") + + lazy val remoteDaemon = { + { + val p = TestProbe()(client) + client.actorSelection(RootActorPath(addr) / receptionist.path.elements).tell(IdentifyReq("/remote"), p.ref) + p.expectMsgType[ActorIdentity].ref.get + } + } + + lazy val target2 = { + val p = TestProbe()(client) + client.actorSelection(RootActorPath(addr) / receptionist.path.elements).tell( + IdentifyReq("child2"), p.ref) + p.expectMsgType[ActorIdentity].ref.get + } override def afterTermination() { - shutdown(other) + shutdown(client) } // need to enable debug log-level without actually printing those messages system.eventStream.publish(TestEvent.Mute(EventFilter.debug())) - // but instead install our own listener - system.eventStream.subscribe(system.actorOf(Props(new Actor { - import Logging._ - def receive = { - case d @ Debug(_, _, msg: String) if msg contains "dropping" ⇒ testActor ! d - case _ ⇒ - } - }).withDeploy(Deploy.local), "debugSniffer"), classOf[Logging.Debug]) - "UntrustedMode" must { + "allow actor selection to configured white list" in { + val sel = client.actorSelection(RootActorPath(addr) / receptionist.path.elements) + sel ! "hello" + expectMsg("hello") + } + "discard harmful messages to /remote" in { - target1 ! "hello" - expectMsgType[Logging.Debug] + val logProbe = TestProbe() + // but instead install our own listener + system.eventStream.subscribe(system.actorOf(Props(new Actor { + import Logging._ + def receive = { + case d @ Debug(_, _, msg: String) if msg contains "dropping" ⇒ logProbe.ref ! d + case _ ⇒ + } + }).withDeploy(Deploy.local), "debugSniffer"), classOf[Logging.Debug]) + + remoteDaemon ! "hello" + logProbe.expectMsgType[Logging.Debug] } "discard harmful messages to testActor" in { - target2 ! Terminated(target1)(existenceConfirmed = true, addressTerminated = false) - expectMsgType[Logging.Debug] + target2 ! Terminated(remoteDaemon)(existenceConfirmed = true, addressTerminated = false) target2 ! PoisonPill - expectMsgType[Logging.Debug] - other.stop(target2) - expectMsgType[Logging.Debug] + client.stop(target2) target2 ! "blech" expectMsg("blech") } "discard watch messages" in { - other.actorOf(Props(new Actor { + client.actorOf(Props(new Actor { context.watch(target2) def receive = { case x ⇒ testActor forward x } }).withDeploy(Deploy.local)) - within(1.second) { - expectMsgType[Logging.Debug] - expectNoMsg - } + receptionist ! StopChild("child2") + expectMsg("child2 stopped") + // no Terminated msg, since watch was discarded + expectNoMsg(1.second) + } + + "discard actor selection" in { + val sel = client.actorSelection(RootActorPath(addr) / testActor.path.elements) + sel ! "hello" + expectNoMsg(1.second) + } + + "discard actor selection with non root anchor" in { + val p = TestProbe()(client) + client.actorSelection(RootActorPath(addr) / receptionist.path.elements).tell( + Identify(None), p.ref) + val clientReceptionistRef = p.expectMsgType[ActorIdentity].ref.get + + val sel = ActorSelection(clientReceptionistRef, receptionist.path.elements.mkString("/", "/", "")) + sel ! "hello" + expectNoMsg(1.second) + } + + "discard actor selection to child of matching white list" in { + val sel = client.actorSelection(RootActorPath(addr) / receptionist.path.elements / "child1") + sel ! "hello" + expectNoMsg(1.second) + } + + "discard actor selection with wildcard" in { + val sel = client.actorSelection(RootActorPath(addr) / receptionist.path.elements / "*") + sel ! "hello" + expectNoMsg(1.second) + } + + "discard actor selection containing harmful message" in { + val sel = client.actorSelection(RootActorPath(addr) / receptionist.path.elements) + sel ! PoisonPill + expectNoMsg(1.second) } }