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

jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new bc5b6a2d162 Fix subscription topic authorization bypass (#18418)
bc5b6a2d162 is described below

commit bc5b6a2d162fa1f38c0553fe177430629bf55262
Author: Caideyipi <[email protected]>
AuthorDate: Mon Aug 10 09:47:35 2026 +0800

    Fix subscription topic authorization bypass (#18418)
---
 .../protocol/thrift/impl/ClientRPCServiceImpl.java |   2 +-
 .../agent/SubscriptionReceiverAgent.java           |  11 +++
 .../subscription/agent/SubscriptionTopicAgent.java | 105 +++++++++++++++++++++
 .../receiver/SubscriptionReceiver.java             |   2 +
 .../receiver/SubscriptionReceiverV1.java           |  53 +++++++++++
 5 files changed, 172 insertions(+), 1 deletion(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/ClientRPCServiceImpl.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/ClientRPCServiceImpl.java
index 1752c8acf76..790e022ded3 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/ClientRPCServiceImpl.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/thrift/impl/ClientRPCServiceImpl.java
@@ -3485,7 +3485,7 @@ public class ClientRPCServiceImpl implements 
IClientRPCServiceWithHandler {
         return getNotLoggedInPipeSubscribeResp();
       }
 
-      return SubscriptionAgent.receiver().handle(req);
+      return SubscriptionAgent.receiver().handle(req, 
clientSession.getUsername());
     } finally {
       SESSION_MANAGER.updateIdleTime();
     }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgent.java
index 4b26a566837..192a07dc83a 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionReceiverAgent.java
@@ -78,6 +78,16 @@ public class SubscriptionReceiverAgent {
   }
 
   public TPipeSubscribeResp handle(final TPipeSubscribeReq req) {
+    return handle(req, null);
+  }
+
+  public TPipeSubscribeResp handle(final TPipeSubscribeReq req, final String 
username) {
+    if (username == null) {
+      return new TPipeSubscribeResp(
+          RpcUtils.getStatus(TSStatusCode.NO_PERMISSION),
+          PipeSubscribeResponseVersion.VERSION_1.getVersion(),
+          PipeSubscribeResponseType.ACK.getType());
+    }
     if (!SubscriptionConfig.getInstance().getSubscriptionEnabled()) {
       return SUBSCRIPTION_NOT_ENABLED_ERROR_RESP;
     }
@@ -85,6 +95,7 @@ public class SubscriptionReceiverAgent {
     final byte reqVersion = req.getVersion();
     if (RECEIVER_CONSTRUCTORS.containsKey(reqVersion)) {
       final SubscriptionReceiver receiver = getReceiver(reqVersion);
+      receiver.setAuthenticatedUsername(username);
       activeReceivers.add(receiver);
       receiver.handleTimeout();
       return receiver.handle(req);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java
index 3f69633f499..256c87e521d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/agent/SubscriptionTopicAgent.java
@@ -20,9 +20,15 @@
 package org.apache.iotdb.db.subscription.agent;
 
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.auth.entity.PrivilegeType;
+import org.apache.iotdb.commons.path.PartialPath;
 import org.apache.iotdb.commons.pipe.config.constant.SystemConstant;
+import org.apache.iotdb.commons.pipe.datastructure.pattern.IoTDBTreePattern;
+import org.apache.iotdb.commons.pipe.datastructure.pattern.PrefixTreePattern;
+import org.apache.iotdb.commons.pipe.datastructure.pattern.TreePattern;
 import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta;
 import org.apache.iotdb.commons.subscription.meta.topic.TopicMetaKeeper;
+import org.apache.iotdb.db.auth.AuthorityChecker;
 import org.apache.iotdb.db.i18n.DataNodeMiscMessages;
 import org.apache.iotdb.db.i18n.DataNodePipeMessages;
 import org.apache.iotdb.mpp.rpc.thrift.TPushTopicMetaRespExceptionMessage;
@@ -369,6 +375,105 @@ public class SubscriptionTopicAgent {
     return RpcUtils.SUCCESS_STATUS;
   }
 
+  /**
+   * Check that the authenticated session can read all data covered by the 
requested topics.
+   * ConsumerConfig is client-controlled and therefore must not be used as the 
authorization
+   * identity.
+   */
+  public TSStatus checkTopicReadPermissions(
+      final String username,
+      final ConsumerConfig consumerConfig,
+      final Iterable<String> topicNames) {
+    if (Objects.isNull(username)) {
+      return RpcUtils.getStatus(TSStatusCode.NO_PERMISSION);
+    }
+
+    acquireReadLock();
+    try {
+      for (final String topicName : topicNames) {
+        final TopicMeta topicMeta =
+            topicMetaKeeper.getTopicMeta(topicName, 
isTableModel(consumerConfig));
+        if (Objects.isNull(topicMeta)) {
+          continue;
+        }
+
+        final TSStatus status =
+            topicMeta.getConfig().isTableTopic()
+                ? checkTableTopicReadPermission(username, topicMeta)
+                : checkTreeTopicReadPermission(username, topicMeta);
+        if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+          return status;
+        }
+      }
+      return RpcUtils.SUCCESS_STATUS;
+    } finally {
+      releaseReadLock();
+    }
+  }
+
+  private TSStatus checkTreeTopicReadPermission(final String username, final 
TopicMeta topicMeta) {
+    final TopicConfig topicConfig = topicMeta.getConfig();
+    final TreePattern treePattern =
+        topicConfig.getAttribute().containsKey(TopicConstant.PATTERN_KEY)
+            ? new 
PrefixTreePattern(topicConfig.getAttribute().get(TopicConstant.PATTERN_KEY))
+            : new IoTDBTreePattern(
+                topicConfig.getStringOrDefault(
+                    TopicConstant.PATH_KEY, TopicConstant.PATH_DEFAULT_VALUE));
+    for (final PartialPath path : treePattern.getBaseInclusionPaths()) {
+      if (!AuthorityChecker.checkFullPathOrPatternPermission(
+          username, path, PrivilegeType.READ_DATA)) {
+        return AuthorityChecker.getTSStatus(false, path, 
PrivilegeType.READ_DATA);
+      }
+    }
+    return RpcUtils.SUCCESS_STATUS;
+  }
+
+  private TSStatus checkTableTopicReadPermission(final String username, final 
TopicMeta topicMeta) {
+    if (AuthorityChecker.SUPER_USER.equals(username)) {
+      return RpcUtils.SUCCESS_STATUS;
+    }
+    final TopicConfig topicConfig = topicMeta.getConfig();
+    final String database =
+        topicConfig.getStringOrDefault(
+            TopicConstant.DATABASE_KEY, TopicConstant.DATABASE_DEFAULT_VALUE);
+    final String table =
+        topicConfig.getStringOrDefault(TopicConstant.TABLE_KEY, 
TopicConstant.TABLE_DEFAULT_VALUE);
+
+    // A database-level SELECT grant covers all tables in one database. For a 
topic whose
+    // database/table is a regular expression, only an any-scope SELECT grant 
is broad enough to
+    // cover every object matched by the topic.
+    final boolean databasePattern = isRegexPattern(database);
+    final boolean tablePattern = isRegexPattern(table);
+    final boolean allowed =
+        (databasePattern
+            ? AuthorityChecker.checkDBPermission(
+                username, AuthorityChecker.ANY_SCOPE, PrivilegeType.SELECT)
+            : AuthorityChecker.checkDBPermission(username, database, 
PrivilegeType.SELECT)
+                || (!tablePattern
+                    && AuthorityChecker.checkTablePermission(
+                        username, database, table, PrivilegeType.SELECT)));
+    return allowed
+        ? RpcUtils.SUCCESS_STATUS
+        : AuthorityChecker.getTSStatus(false, PrivilegeType.SELECT, database, 
table);
+  }
+
+  private static boolean isRegexPattern(final String value) {
+    return value.indexOf('.') >= 0
+        || value.indexOf('*') >= 0
+        || value.indexOf('+') >= 0
+        || value.indexOf('?') >= 0
+        || value.indexOf('[') >= 0
+        || value.indexOf(']') >= 0
+        || value.indexOf('(') >= 0
+        || value.indexOf(')') >= 0
+        || value.indexOf('{') >= 0
+        || value.indexOf('}') >= 0
+        || value.indexOf('|') >= 0
+        || value.indexOf('^') >= 0
+        || value.indexOf('$') >= 0
+        || value.indexOf('\\') >= 0;
+  }
+
   /**
    * Apply owner lease renewals pushed by ConfigNode via the dedicated 
subscription owner heartbeat.
    * The pushed remaining duration is converted to a DataNode-local expire 
time on the local clock,
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiver.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiver.java
index 36e3c9b74f5..cc7b57eee81 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiver.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiver.java
@@ -27,6 +27,8 @@ public interface SubscriptionReceiver {
 
   TPipeSubscribeResp handle(TPipeSubscribeReq req);
 
+  void setAuthenticatedUsername(final String username);
+
   PipeSubscribeRequestVersion getVersion();
 
   void handleExit();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
index 268bb8df119..4ca93fb5cb8 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/receiver/SubscriptionReceiverV1.java
@@ -123,6 +123,7 @@ public class SubscriptionReceiverV1 implements 
SubscriptionReceiver {
 
   private final ThreadLocal<ConsumerConfig> consumerConfigThreadLocal = new 
ThreadLocal<>();
   private final ThreadLocal<PollTimer> pollTimerThreadLocal = new 
ThreadLocal<>();
+  private volatile String authenticatedUsername;
   private volatile ConsumerConfig sharedConsumerConfig;
   private volatile boolean consumerInvalidated;
   private volatile long lastActivityTimeMs = System.currentTimeMillis();
@@ -180,6 +181,11 @@ public class SubscriptionReceiverV1 implements 
SubscriptionReceiver {
     return PipeSubscribeRequestVersion.VERSION_1;
   }
 
+  @Override
+  public void setAuthenticatedUsername(final String username) {
+    authenticatedUsername = username;
+  }
+
   @Override
   public void handleExit() {
     final ConsumerConfig consumerConfig = consumerConfigThreadLocal.get();
@@ -197,6 +203,7 @@ public class SubscriptionReceiverV1 implements 
SubscriptionReceiver {
       consumerConfigThreadLocal.remove();
     }
     clearSharedConsumerState();
+    authenticatedUsername = null;
   }
 
   @Override
@@ -369,6 +376,13 @@ public class SubscriptionReceiverV1 implements 
SubscriptionReceiver {
       return PipeSubscribeHeartbeatResp.toTPipeSubscribeResp(ownerStatus);
     }
 
+    final TSStatus readPermissionStatus =
+        SubscriptionAgent.topic()
+            .checkTopicReadPermissions(authenticatedUsername, consumerConfig, 
subscribedTopicNames);
+    if (readPermissionStatus.getCode() != 
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+      return 
PipeSubscribeHeartbeatResp.toTPipeSubscribeResp(readPermissionStatus);
+    }
+
     LOGGER.info(DataNodeMiscMessages.SUBSCRIPTION_CONSUMER_HEARTBEAT_SUCCESS, 
consumerConfig);
 
     // fetch subscribed topics
@@ -453,6 +467,12 @@ public class SubscriptionReceiverV1 implements 
SubscriptionReceiver {
     if (ownerStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
       return PipeSubscribeSubscribeResp.toTPipeSubscribeResp(ownerStatus);
     }
+    final TSStatus readPermissionStatus =
+        SubscriptionAgent.topic()
+            .checkTopicReadPermissions(authenticatedUsername, consumerConfig, 
topicNames);
+    if (readPermissionStatus.getCode() != 
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+      return 
PipeSubscribeSubscribeResp.toTPipeSubscribeResp(readPermissionStatus);
+    }
     subscribe(consumerConfig, topicNames);
 
     LOGGER.info(
@@ -564,6 +584,14 @@ public class SubscriptionReceiverV1 implements 
SubscriptionReceiver {
           if (ownerStatus.getCode() != 
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
             return PipeSubscribePollResp.toTPipeSubscribeResp(ownerStatus, 
Collections.emptyList());
           }
+          final TSStatus readPermissionStatus =
+              SubscriptionAgent.topic()
+                  .checkTopicReadPermissions(
+                      authenticatedUsername, consumerConfig, 
topicNamesToCheck);
+          if (readPermissionStatus.getCode() != 
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+            return PipeSubscribePollResp.toTPipeSubscribeResp(
+                readPermissionStatus, Collections.emptyList());
+          }
           events =
               handlePipeSubscribePollRequest(
                   consumerConfig,
@@ -581,6 +609,18 @@ public class SubscriptionReceiverV1 implements 
SubscriptionReceiver {
             return PipeSubscribePollResp.toTPipeSubscribeResp(
                 tsFileOwnerStatus, Collections.emptyList());
           }
+          final String tsFileTopicName =
+              ((PollFilePayload) 
request.getPayload()).getCommitContext().getTopicName();
+          final TSStatus tsFileReadPermissionStatus =
+              SubscriptionAgent.topic()
+                  .checkTopicReadPermissions(
+                      authenticatedUsername,
+                      consumerConfig,
+                      Collections.singleton(tsFileTopicName));
+          if (tsFileReadPermissionStatus.getCode() != 
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+            return PipeSubscribePollResp.toTPipeSubscribeResp(
+                tsFileReadPermissionStatus, Collections.emptyList());
+          }
           events =
               handlePipeSubscribePollTsFileRequest(
                   consumerConfig, (PollFilePayload) request.getPayload());
@@ -597,6 +637,19 @@ public class SubscriptionReceiverV1 implements 
SubscriptionReceiver {
             return PipeSubscribePollResp.toTPipeSubscribeResp(
                 tabletsOwnerStatus, Collections.emptyList());
           }
+          final String tabletsTopicName =
+              ((PollTabletsPayload) 
request.getPayload()).getCommitContext().getTopicName();
+          final TSStatus tabletsReadPermissionStatus =
+              SubscriptionAgent.topic()
+                  .checkTopicReadPermissions(
+                      authenticatedUsername,
+                      consumerConfig,
+                      Collections.singleton(tabletsTopicName));
+          if (tabletsReadPermissionStatus.getCode()
+              != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+            return PipeSubscribePollResp.toTPipeSubscribeResp(
+                tabletsReadPermissionStatus, Collections.emptyList());
+          }
           events =
               handlePipeSubscribePollTabletsRequest(
                   consumerConfig, (PollTabletsPayload) request.getPayload());

Reply via email to