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

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/helix.git


The following commit(s) were added to refs/heads/master by this push:
     new 8014dd10f Harden operation class loading in take/free instance APIs
8014dd10f is described below

commit 8014dd10fcdb7fca561606862c53fdbfe5416352
Author: Junkai Xue <[email protected]>
AuthorDate: Tue Nov 3 11:39:31 2026 -0800

    Harden operation class loading in take/free instance APIs
    
    The take/free instance APIs load classes named in the "operation_list"
    request field via loadClass(...).newInstance() with no validation. A
    caller could name any class on the classpath and have its no-arg
    constructor executed before the cast to OperationInterface failed.
    
    Validate the type before instantiating so a class that does not
    implement OperationInterface never has its constructor run, and add an
    optional comma-separated allowlist (rest.operation.class.allowlist). The
    allowlist is unset by default, preserving existing behavior for
    operators who supply their own OperationInterface implementations.
    
    Trim the request class name before the allowlist lookup so it matches
    trimmed allowlist entries, and unwrap InvocationTargetException so a
    legitimate operation constructor's real failure reason is surfaced.
---
 .../MaintenanceManagementService.java              | 49 ++++++++++++++++++++--
 .../helix/rest/common/RestSystemPropertyKeys.java  |  6 +++
 .../TestMaintenanceManagementService.java          | 41 +++++++++++++++++-
 3 files changed, 92 insertions(+), 4 deletions(-)

diff --git 
a/helix-rest/src/main/java/org/apache/helix/rest/clusterMaintenanceService/MaintenanceManagementService.java
 
b/helix-rest/src/main/java/org/apache/helix/rest/clusterMaintenanceService/MaintenanceManagementService.java
index 8d5d025b3..43edb5e89 100644
--- 
a/helix-rest/src/main/java/org/apache/helix/rest/clusterMaintenanceService/MaintenanceManagementService.java
+++ 
b/helix-rest/src/main/java/org/apache/helix/rest/clusterMaintenanceService/MaintenanceManagementService.java
@@ -20,6 +20,7 @@ package org.apache.helix.rest.clusterMaintenanceService;
  */
 
 import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -56,6 +57,7 @@ import org.apache.helix.rest.client.CustomRestClient;
 import org.apache.helix.rest.client.CustomRestClientFactory;
 import org.apache.helix.rest.clusterMaintenanceService.api.OperationInterface;
 import org.apache.helix.rest.common.HelixDataAccessorWrapper;
+import org.apache.helix.rest.common.RestSystemPropertyKeys;
 import org.apache.helix.rest.common.datamodel.RestSnapShot;
 import org.apache.helix.rest.server.json.instance.InstanceInfo;
 import org.apache.helix.rest.server.json.instance.StoppableCheck;
@@ -337,13 +339,39 @@ public class MaintenanceManagementService {
 
   private List<OperationInterface> getAllOperationClasses(List<String> 
operations) {
     List<OperationInterface> operationAbstractClassList = new ArrayList<>();
-    for (String operationClassName : operations) {
+    Set<String> allowlist = getOperationClassAllowlist();
+    for (String operationClassNameRaw : operations) {
+      String operationClassName =
+          operationClassNameRaw == null ? null : operationClassNameRaw.trim();
       try {
+        if (!allowlist.isEmpty() && !allowlist.contains(operationClassName)) {
+          throw new HelixException(
+              String.format("Operation class is not in the configured 
allowlist: %s",
+                  operationClassName));
+        }
         LOG.info("Loading class: " + operationClassName);
+        Class<?> operationClass = HelixUtil.loadClass(getClass(), 
operationClassName);
+        // Validate the type BEFORE instantiating so that a class which does 
not implement
+        // OperationInterface never has its constructor executed.
+        if (!OperationInterface.class.isAssignableFrom(operationClass)) {
+          throw new HelixException(
+              String.format("Operation class does not implement 
OperationInterface: %s",
+                  operationClassName));
+        }
         OperationInterface userOperation =
-            (OperationInterface) HelixUtil.loadClass(getClass(), 
operationClassName)
-                .newInstance();
+            (OperationInterface) 
operationClass.getDeclaredConstructor().newInstance();
         operationAbstractClassList.add(userOperation);
+      } catch (HelixException e) {
+        LOG.error("Rejected operation class: {}. message: ", 
operationClassName, e);
+        throw e;
+      } catch (InvocationTargetException e) {
+        // Unwrap so the operation class constructor's real failure reason is 
surfaced.
+        Throwable cause = e.getCause() == null ? e : e.getCause();
+        LOG.error("Operation class constructor failed for: {}. message: ", 
operationClassName,
+            cause);
+        throw new HelixException(
+            String.format("Operation class constructor failed for: %s. 
message: %s",
+                operationClassName, cause));
       } catch (Exception e) {
         LOG.error("No operation class found for: {}. message: ", 
operationClassName, e);
         throw new HelixException(
@@ -353,6 +381,21 @@ public class MaintenanceManagementService {
     return operationAbstractClassList;
   }
 
+  private static Set<String> getOperationClassAllowlist() {
+    String raw = 
System.getProperty(RestSystemPropertyKeys.REST_OPERATION_CLASS_ALLOWLIST);
+    if (raw == null || raw.trim().isEmpty()) {
+      return Collections.emptySet();
+    }
+    Set<String> allowlist = new HashSet<>();
+    for (String entry : raw.split(",")) {
+      String trimmed = entry.trim();
+      if (!trimmed.isEmpty()) {
+        allowlist.add(trimmed);
+      }
+    }
+    return allowlist;
+  }
+
   /**
    * {@inheritDoc}
    * Single instance stoppable check implementation is a special case of
diff --git 
a/helix-rest/src/main/java/org/apache/helix/rest/common/RestSystemPropertyKeys.java
 
b/helix-rest/src/main/java/org/apache/helix/rest/common/RestSystemPropertyKeys.java
index 1d1e42ab1..5dc6dd955 100644
--- 
a/helix-rest/src/main/java/org/apache/helix/rest/common/RestSystemPropertyKeys.java
+++ 
b/helix-rest/src/main/java/org/apache/helix/rest/common/RestSystemPropertyKeys.java
@@ -22,4 +22,10 @@ package org.apache.helix.rest.common;
 public class RestSystemPropertyKeys {
   // System property for REST HTTP request timeout
   public static final String REST_HTTP_TIMEOUT_MS = "rest.http.timeout.ms";
+
+  // Optional comma-separated allowlist of fully-qualified operation class 
names that the
+  // take/free instance APIs are permitted to load via "operation_list". When 
set to a non-empty
+  // value, only classes on this list may be instantiated. When unset or 
empty, no name-based
+  // restriction is applied, but every operation class must still implement 
OperationInterface.
+  public static final String REST_OPERATION_CLASS_ALLOWLIST = 
"rest.operation.class.allowlist";
 }
diff --git 
a/helix-rest/src/test/java/org/apache/helix/rest/clusterMaintenanceService/TestMaintenanceManagementService.java
 
b/helix-rest/src/test/java/org/apache/helix/rest/clusterMaintenanceService/TestMaintenanceManagementService.java
index 5801fa231..9a4844a3f 100644
--- 
a/helix-rest/src/test/java/org/apache/helix/rest/clusterMaintenanceService/TestMaintenanceManagementService.java
+++ 
b/helix-rest/src/test/java/org/apache/helix/rest/clusterMaintenanceService/TestMaintenanceManagementService.java
@@ -352,6 +352,33 @@ public class TestMaintenanceManagementService {
     Assert.assertEquals(instanceInfo2.getOperationResult(), 
"DummyTakeOperationResult");
   }
 
+  /*
+   * A class that does not implement OperationInterface must be rejected 
before its constructor
+   * runs, so that arbitrary classpath classes cannot be instantiated through 
"operation_list".
+   */
+  @Test
+  public void testTakeInstanceRejectsNonOperationClass() throws IOException {
+    Map<String, String> dataMap =
+        ImmutableMap.of("param1", "true", "param2", "faketype", "check1", 
"false", "check2",
+            "true");
+    String jsonContent = new ObjectMapper().writeValueAsString(dataMap);
+    ZKHelixDataAccessor zkHelixDataAccessor = mock(ZKHelixDataAccessor.class);
+    MockMaintenanceManagementService service =
+        new MockMaintenanceManagementService(zkHelixDataAccessor, 
_configAccessor,
+            _customRestClient, true, false, 
HelixRestNamespace.DEFAULT_NAMESPACE_NAME);
+
+    ForbiddenConstructorProbe.invoked = false;
+    MaintenanceManagementInstanceInfo info =
+        service.takeInstance(TEST_CLUSTER, TEST_INSTANCE, 
Collections.emptyList(),
+            MaintenanceManagementService.getMapFromJsonPayload(jsonContent),
+            
Collections.singletonList(ForbiddenConstructorProbe.class.getName()),
+            Collections.EMPTY_MAP, true);
+    Assert.assertFalse(info.isSuccessful(),
+        "Taking an instance with a non-OperationInterface class must fail");
+    Assert.assertFalse(ForbiddenConstructorProbe.invoked,
+        "Constructor of a non-OperationInterface class must not run");
+  }
+
   /*
    * Tests stoppable check api when all checks query is enabled. After helix 
own check fails,
    * the subsequent checks should be performed.
@@ -538,4 +565,16 @@ public class TestMaintenanceManagementService {
     verify(_customRestClient, times(1)).getInstanceStoppableCheck(any(), 
any());
     verify(_customRestClient, times(0)).getPartitionStoppableCheck(any(), 
any(), any());
   }
-}
\ No newline at end of file
+
+  /*
+   * A plain class (NOT an OperationInterface) whose constructor records if it 
ever ran.
+   * Used to prove that rejected operation classes are never instantiated.
+   */
+  public static class ForbiddenConstructorProbe {
+    static volatile boolean invoked = false;
+
+    public ForbiddenConstructorProbe() {
+      invoked = true;
+    }
+  }
+}

Reply via email to