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

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


The following commit(s) were added to refs/heads/main by this push:
     new 50c18eb6b6 [CELEBORN-2381] Authorize PbApplicationMetaRequest to 
prevent secret disclosure
50c18eb6b6 is described below

commit 50c18eb6b66c1926d037441d954270b60f6f638f
Author: Nicholas Jiang <[email protected]>
AuthorDate: Tue Jul 14 21:58:10 2026 +0800

    [CELEBORN-2381] Authorize PbApplicationMetaRequest to prevent secret 
disclosure
    
    ### What changes were proposed in this pull request?
    
    Call `checkAuth(context, appId)` before serving `PbApplicationMetaRequest` 
in the Master, consistent with the other application-scoped handlers 
(`RequestSlots`, `UnregisterShuffle`, `ApplicationLost`).
    
    Workers fetch application meta over the internal channel, where the 
connection has no per-application client id, so the check is a no-op for them; 
it only rejects an external application that asks for another application's 
secret.
    
    ### Why are the changes needed?
    
    The Master served `PbApplicationMetaRequest` by returning the requested 
application's SASL secret without an authorization check. With authentication 
enabled, a caller on the external application port could read another 
application's secret and impersonate it.
    
    ### Does this PR resolve a correctness bug?
    
    - [ ] Yes
    
    ### Does this PR introduce _any_ user-facing change?
    
    - [ ] Yes
    
    ### How was this patch tested?
    
    - New `MasterApplicationMetaAuthSuite` verifying `PbApplicationMetaRequest` 
is authorized against the registered application.
    - New case in `MasterSuite` verifying a caller requesting another 
application's secret is rejected.
    
    Closes #3759 from SteNicholas/CELEBORN-2381.
    
    Authored-by: Nicholas Jiang <[email protected]>
    Signed-off-by: Shuang <[email protected]>
---
 .../celeborn/service/deploy/master/Master.scala    |   5 +-
 .../master/MasterApplicationMetaAuthSuite.scala    | 146 +++++++++++++++++++++
 .../service/deploy/master/MasterSuite.scala        |  50 ++++++-
 3 files changed, 197 insertions(+), 4 deletions(-)

diff --git 
a/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala 
b/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
index 6b25552060..8960934dd3 100644
--- 
a/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
+++ 
b/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
@@ -647,7 +647,10 @@ private[celeborn] class Master(
           context))
 
     case pb: PbApplicationMetaRequest =>
-      // This request is from a worker
+      // Workers fetch application meta over the internal channel, where no 
client id
+      // is set, so the check is a no-op for them; it only rejects an external
+      // application that asks for another application's secret.
+      checkAuth(context, pb.getAppId)
       executeWithLeaderChecker(context, 
handleRequestForApplicationMeta(context, pb))
 
     case pb: PbRemoveWorkersUnavailableInfo =>
diff --git 
a/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterApplicationMetaAuthSuite.scala
 
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterApplicationMetaAuthSuite.scala
new file mode 100644
index 0000000000..b672851cb0
--- /dev/null
+++ 
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterApplicationMetaAuthSuite.scala
@@ -0,0 +1,146 @@
+/*
+ * 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.celeborn.service.deploy.master
+
+import java.io.{PrintWriter, StringWriter}
+
+import scala.collection.mutable.ArrayBuffer
+
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.funsuite.AnyFunSuite
+
+import org.apache.celeborn.common.CelebornConf
+import org.apache.celeborn.common.metrics.source.Role
+import org.apache.celeborn.common.network.sasl.registration.RegistrationInfo
+import org.apache.celeborn.common.protocol.{PbApplicationMeta, 
PbApplicationMetaRequest, RpcNameConstants, TransportModuleConstants}
+import org.apache.celeborn.common.rpc.{ClientSaslContextBuilder, RpcAddress, 
RpcEndpointRef, RpcEnv, RpcSecurityContextBuilder}
+
+/**
+ * End-to-end authorization check for PbApplicationMetaRequest with auth 
enabled, driving
+ * the real SASL registration path rather than a mocked client id (as in 
[[MasterSuite]]).
+ *
+ * It covers the whole chain the security guarantee rests on: registration 
sets the
+ * connection's client id, and checkAuth enforces it. A regression that 
stopped setting
+ * the client id would silently turn checkAuth into a no-op and reopen the 
cross-tenant
+ * secret leak while the mocked unit test still passed; this suite catches 
that.
+ */
+class MasterApplicationMetaAuthSuite extends AnyFunSuite
+  with BeforeAndAfterAll
+  with MasterClusterFeature {
+
+  private val conf = new CelebornConf()
+    .set(CelebornConf.AUTH_ENABLED.key, "true")
+    .set(CelebornConf.INTERNAL_PORT_ENABLED.key, "true")
+
+  private var master: Master = _
+  private var externalAddress: RpcAddress = _
+  private var internalAddress: RpcAddress = _
+  private val clientEnvs = new ArrayBuffer[RpcEnv]()
+
+  override def beforeAll(): Unit = {
+    super.beforeAll()
+    master = setupMasterWithRandomPort(conf.getAll.toMap)
+    externalAddress = master.rpcEnv.address
+    internalAddress = master.internalRpcEnvInUse.address
+  }
+
+  override def afterAll(): Unit = {
+    clientEnvs.foreach(_.shutdown())
+    if (master != null) {
+      shutdownMaster()
+    }
+    super.afterAll()
+  }
+
+  private def metaRequest(appId: String): PbApplicationMetaRequest =
+    PbApplicationMetaRequest.newBuilder().setAppId(appId).build()
+
+  // A client env that authenticates and registers `appId` with the master 
over the
+  // external port, exactly as an application (LifecycleManager) does.
+  private def registeredAppRef(appId: String, secret: String): RpcEndpointRef 
= {
+    val securityContext = new RpcSecurityContextBuilder()
+      .withClientSaslContext(
+        new ClientSaslContextBuilder()
+          .withAddRegistrationBootstrap(true)
+          .withAppId(appId)
+          .withSaslUser(appId)
+          .withSaslPassword(secret)
+          .withRegistrationInfo(new RegistrationInfo())
+          .build())
+      .build()
+    val env = RpcEnv.create(
+      s"client-$appId",
+      TransportModuleConstants.RPC_SERVICE_MODULE,
+      "localhost",
+      0,
+      conf,
+      Role.CLIENT,
+      Some(securityContext))
+    clientEnvs += env
+    env.setupEndpointRef(externalAddress, RpcNameConstants.MASTER_EP)
+  }
+
+  // A worker-like env reaching the master over the unauthenticated internal 
port, where
+  // no per-application client id is set on the connection.
+  private def workerInternalRef(): RpcEndpointRef = {
+    val env = RpcEnv.create(
+      "worker-internal",
+      TransportModuleConstants.RPC_SERVICE_MODULE,
+      "localhost",
+      0,
+      conf,
+      Role.WORKER,
+      None)
+    clientEnvs += env
+    env.setupEndpointRef(internalAddress, RpcNameConstants.MASTER_INTERNAL_EP)
+  }
+
+  private def stackTraceOf(t: Throwable): String = {
+    val sw = new StringWriter()
+    t.printStackTrace(new PrintWriter(sw))
+    sw.toString
+  }
+
+  test("PbApplicationMetaRequest is authorized against the registered 
application") {
+    val victimApp = "victim-app"
+    val victimSecret = "victim-secret"
+    val attackerApp = "attacker-app"
+    val attackerSecret = "attacker-secret"
+
+    val victimRef = registeredAppRef(victimApp, victimSecret)
+    // Reading its own meta confirms the victim's secret is planted in the 
master.
+    
assert(victimRef.askSync[PbApplicationMeta](metaRequest(victimApp)).getSecret 
== victimSecret)
+
+    // The attacker successfully registers its own app (so the connection is 
genuine and
+    // authenticated), but the registration path set its client id to the 
attacker's app,
+    // so checkAuth rejects the cross-application read of the victim's secret.
+    val attackerRef = registeredAppRef(attackerApp, attackerSecret)
+    assert(
+      
attackerRef.askSync[PbApplicationMeta](metaRequest(attackerApp)).getSecret == 
attackerSecret)
+    val e = intercept[Exception] {
+      attackerRef.askSync[PbApplicationMeta](metaRequest(victimApp))
+    }
+    assert(stackTraceOf(e).contains(s"not authorized for application 
$victimApp"))
+
+    // A worker over the internal port carries no client id, so it can still 
fetch the
+    // victim's meta — the legitimate path must keep working.
+    assert(
+      
workerInternalRef().askSync[PbApplicationMeta](metaRequest(victimApp)).getSecret
+        == victimSecret)
+  }
+}
diff --git 
a/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
 
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
index 0a8a7b592f..d915320c95 100644
--- 
a/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
+++ 
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSuite.scala
@@ -21,16 +21,18 @@ import java.nio.file.Files
 import java.util
 
 import org.mockito.ArgumentCaptor
-import org.mockito.Mockito.{mock, verify}
+import org.mockito.Mockito.{mock, verify, when}
 import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
 import org.scalatest.funsuite.AnyFunSuite
 
 import org.apache.celeborn.common.CelebornConf
 import org.apache.celeborn.common.identity.UserIdentifier
-import org.apache.celeborn.common.protocol.{PbCheckForWorkerTimeout, 
PbRegisterWorker}
+import org.apache.celeborn.common.network.client.{RpcResponseCallback, 
TransportClient}
+import org.apache.celeborn.common.protocol.{PbApplicationMetaRequest, 
PbCheckForWorkerTimeout, PbRegisterWorker}
 import 
org.apache.celeborn.common.protocol.message.ControlMessages.{RequestSlots, 
RequestSlotsResponse}
 import org.apache.celeborn.common.protocol.message.StatusCode
-import org.apache.celeborn.common.rpc.RpcCallContext
+import org.apache.celeborn.common.rpc.{RpcAddress, RpcCallContext}
+import org.apache.celeborn.common.rpc.netty.{NettyRpcEnv, 
RemoteNettyRpcCallContext}
 import org.apache.celeborn.common.util.{CelebornExitKind, ThreadUtils}
 
 class MasterSuite extends AnyFunSuite
@@ -197,4 +199,46 @@ class MasterSuite extends AnyFunSuite
 
     master.rpcEnv.shutdown()
   }
+
+  test("PbApplicationMetaRequest rejects a caller requesting another 
application's secret") {
+    val conf = new CelebornConf()
+    val randomMasterPort = selectRandomPort()
+    val randomHttpPort = selectRandomPort()
+    conf.set(CelebornConf.HA_ENABLED.key, "false")
+    conf.set(CelebornConf.MASTER_HTTP_HOST.key, "127.0.0.1")
+    conf.set(CelebornConf.MASTER_HTTP_PORT.key, randomHttpPort.toString)
+
+    val args = Array("-h", "localhost", "-p", randomMasterPort.toString)
+    val masterArgs = new MasterArguments(args, conf)
+    val master = new Master(conf, masterArgs)
+
+    // Builds a remote call context whose connection is authenticated as 
`clientId`;
+    // null models a worker on the internal channel, which sets no client id.
+    def contextForClient(clientId: String): RemoteNettyRpcCallContext = {
+      val client = mock(classOf[TransportClient])
+      when(client.getClientId).thenReturn(clientId)
+      new RemoteNettyRpcCallContext(
+        mock(classOf[NettyRpcEnv]),
+        mock(classOf[RpcResponseCallback]),
+        RpcAddress("localhost", 1234),
+        client)
+    }
+
+    val request = 
PbApplicationMetaRequest.newBuilder().setAppId("victim-app").build()
+    val unhandled = (_: Any) => fail("PbApplicationMetaRequest was not 
handled")
+
+    try {
+      // An application authenticated as "attacker-app" on the external port 
must not
+      // be able to read "victim-app"'s secret.
+      val e = intercept[IllegalStateException] {
+        
master.receiveAndReply(contextForClient("attacker-app")).applyOrElse(request, 
unhandled)
+      }
+      assert(e.getMessage.contains("not authorized for application 
victim-app"))
+
+      // A worker carries no client id, so the guard is a no-op and the 
request is served.
+      master.receiveAndReply(contextForClient(null)).applyOrElse(request, 
unhandled)
+    } finally {
+      master.rpcEnv.shutdown()
+    }
+  }
 }

Reply via email to