From e88696b35bf68f38823246dc8b41a2c5abc7fff9 Mon Sep 17 00:00:00 2001
From: yunhanw <yunhanw@google.com>
Date: Mon, 29 Aug 2022 12:34:12 -0700
Subject: [PATCH 1/2] Prevent Subscription to attributes with no access
 privilege

---
 src/app/InteractionModelEngine.cpp            | 142 +++++++--
 src/app/InteractionModelEngine.h              |  22 ++
 src/app/MessageDef/AttributePathIB.cpp        |  57 ++++
 src/app/MessageDef/AttributePathIB.h          |   8 +
 src/app/MessageDef/EventPathIB.cpp            |  46 +++
 src/app/MessageDef/EventPathIB.h              |   8 +
 src/app/ReadHandler.cpp                       | 114 +------
 src/app/ReadHandler.h                         |   2 +-
 src/app/tests/BUILD.gn                        |   2 +
 src/app/tests/TestAclAttribute.cpp            | 283 ++++++++++++++++++
 src/app/tests/TestAclEvent.cpp                |   8 +-
 src/app/tests/TestReadInteraction.cpp         |  47 +++
 .../tests/integration/chip_im_initiator.cpp   |   5 +
 .../tests/integration/chip_im_responder.cpp   |   5 +
 .../util/ember-compatibility-functions.cpp    |  12 +
 src/controller/tests/TestReadChunking.cpp     |  29 +-
 src/controller/tests/data_model/TestRead.cpp  |  56 +++-
 src/darwin/Framework/CHIP/MTRIMDispatch.mm    |   5 +
 18 files changed, 692 insertions(+), 159 deletions(-)
 create mode 100644 src/app/tests/TestAclAttribute.cpp

diff --git a/src/app/InteractionModelEngine.cpp b/src/app/InteractionModelEngine.cpp
index 3f88c40f5f9910..61af4c1b19b77b 100644
--- a/src/app/InteractionModelEngine.cpp
+++ b/src/app/InteractionModelEngine.cpp
@@ -27,6 +27,9 @@
 
 #include <cinttypes>
 
+#include "access/RequestPath.h"
+#include "access/SubjectDescriptor.h"
+#include <app/RequiredPrivilege.h>
 #include <lib/core/CHIPTLVUtilities.hpp>
 #include <lib/support/CodeUtils.h>
 
@@ -323,6 +326,79 @@ Status InteractionModelEngine::OnInvokeCommandRequest(Messaging::ExchangeContext
     return Status::Success;
 }
 
+CHIP_ERROR InteractionModelEngine::ParseAttributePaths(const Access::SubjectDescriptor & aSubjectDescriptor,
+                                                       AttributePathIBs::Parser & aAttributePathListParser,
+                                                       bool & aHasValidAttributePath, size_t & aRequestedAttributePathCount)
+{
+    TLV::TLVReader pathReader;
+    aAttributePathListParser.GetReader(&pathReader);
+    CHIP_ERROR err = CHIP_NO_ERROR;
+
+    aHasValidAttributePath       = false;
+    aRequestedAttributePathCount = 0;
+
+    while (CHIP_NO_ERROR == (err = pathReader.Next()))
+    {
+        VerifyOrReturnError(TLV::AnonymousTag() == pathReader.GetTag(), CHIP_ERROR_INVALID_TLV_TAG);
+
+        AttributePathIB::Parser path;
+        //
+        // We create an iterator to point to a single item object list that tracks the path we just parsed.
+        // This avoids the 'parse all paths' approach that is employed in ReadHandler since we want to
+        // avoid allocating out of the path store during this minimal initial processing stage.
+        //
+        ObjectList<AttributePathParams> paramsList;
+
+        ReturnErrorOnFailure(path.Init(pathReader));
+        ReturnErrorOnFailure(path.ParsePath(paramsList.mValue));
+
+        if (paramsList.mValue.IsWildcardPath())
+        {
+            AttributePathExpandIterator pathIterator(&paramsList);
+            ConcreteAttributePath readPath;
+
+            // The definition of "valid path" is "path exists and ACL allows access". The "path exists" part is handled by
+            // AttributePathExpandIterator. So we just need to check the ACL bits.
+            for (; pathIterator.Get(readPath); pathIterator.Next())
+            {
+                Access::RequestPath requestPath{ .cluster = readPath.mClusterId, .endpoint = readPath.mEndpointId };
+                err = Access::GetAccessControl().Check(aSubjectDescriptor, requestPath,
+                                                       RequiredPrivilege::ForReadAttribute(readPath));
+                if (err == CHIP_NO_ERROR)
+                {
+                    aHasValidAttributePath = true;
+                    break;
+                }
+            }
+        }
+        else
+        {
+            ConcreteAttributePath concretePath(paramsList.mValue.mEndpointId, paramsList.mValue.mClusterId,
+                                               paramsList.mValue.mAttributeId);
+            if (ConcreteAttributePathExists(concretePath))
+            {
+                Access::RequestPath requestPath{ .cluster = concretePath.mClusterId, .endpoint = concretePath.mEndpointId };
+
+                err = Access::GetAccessControl().Check(aSubjectDescriptor, requestPath,
+                                                       RequiredPrivilege::ForReadAttribute(concretePath));
+                if (err == CHIP_NO_ERROR)
+                {
+                    aHasValidAttributePath = true;
+                }
+            }
+        }
+
+        aRequestedAttributePathCount++;
+    }
+
+    if (err == CHIP_ERROR_END_OF_TLV)
+    {
+        err = CHIP_NO_ERROR;
+    }
+
+    return err;
+}
+
 Protocols::InteractionModel::Status InteractionModelEngine::OnReadInitialRequest(Messaging::ExchangeContext * apExchangeContext,
                                                                                  const PayloadHeader & aPayloadHeader,
                                                                                  System::PacketBufferHandle && aPayload,
@@ -349,28 +425,25 @@ Protocols::InteractionModel::Status InteractionModelEngine::OnReadInitialRequest
         reader.Init(aPayload.Retain());
 
         SubscribeRequestMessage::Parser subscribeRequestParser;
-        CHIP_ERROR err = subscribeRequestParser.Init(reader);
-        if (err != CHIP_NO_ERROR)
-        {
-            return Status::InvalidAction;
-        }
-
+        VerifyOrReturnError(subscribeRequestParser.Init(reader) == CHIP_NO_ERROR, Status::InvalidAction);
         {
             size_t requestedAttributePathCount = 0;
             size_t requestedEventPathCount     = 0;
             AttributePathIBs::Parser attributePathListParser;
-            err = subscribeRequestParser.GetAttributeRequests(&attributePathListParser);
+            bool hasValidAttributePath = false;
+
+            CHIP_ERROR err = subscribeRequestParser.GetAttributeRequests(&attributePathListParser);
             if (err == CHIP_NO_ERROR)
             {
-                TLV::TLVReader pathReader;
-                attributePathListParser.GetReader(&pathReader);
-                err = TLV::Utilities::Count(pathReader, requestedAttributePathCount, false);
-            }
-            else if (err == CHIP_ERROR_END_OF_TLV)
-            {
-                err = CHIP_NO_ERROR;
+                auto subjectDescriptor = apExchangeContext->GetSessionHandle()->AsSecureSession()->GetSubjectDescriptor();
+                err                    = ParseAttributePaths(subjectDescriptor, attributePathListParser, hasValidAttributePath,
+                                          requestedAttributePathCount);
+                if (err != CHIP_NO_ERROR)
+                {
+                    return Status::InvalidAction;
+                }
             }
-            if (err != CHIP_NO_ERROR)
+            else if (err != CHIP_ERROR_END_OF_TLV)
             {
                 return Status::InvalidAction;
             }
@@ -381,14 +454,34 @@ Protocols::InteractionModel::Status InteractionModelEngine::OnReadInitialRequest
             {
                 TLV::TLVReader pathReader;
                 eventpathListParser.GetReader(&pathReader);
-                err = TLV::Utilities::Count(pathReader, requestedEventPathCount, false);
+                ReturnErrorCodeIf(TLV::Utilities::Count(pathReader, requestedEventPathCount, false) != CHIP_NO_ERROR,
+                                  Status::InvalidAction);
             }
-            else if (err == CHIP_ERROR_END_OF_TLV)
+            else if (err != CHIP_ERROR_END_OF_TLV)
             {
-                err = CHIP_NO_ERROR;
+                return Status::InvalidAction;
             }
-            if (err != CHIP_NO_ERROR)
+
+            if (requestedAttributePathCount == 0 && requestedEventPathCount == 0)
             {
+                ChipLogError(InteractionModel,
+                             "Subscription from [%u:" ChipLogFormatX64 "] has no attribute or event paths. Rejecting request.",
+                             apExchangeContext->GetSessionHandle()->GetFabricIndex(),
+                             ChipLogValueX64(apExchangeContext->GetSessionHandle()->AsSecureSession()->GetPeerNodeId()));
+                return Status::InvalidAction;
+            }
+
+            //
+            // TODO: We don't have an easy way to do a similar 'path expansion' for events to deduce
+            // access so for now, assume that the presence of any path in the event list means they
+            // might have some access to those events.
+            //
+            if (!hasValidAttributePath && requestedEventPathCount == 0)
+            {
+                ChipLogError(InteractionModel,
+                             "Subscription from [%u:" ChipLogFormatX64 "] has no access at all. Rejecting request.",
+                             apExchangeContext->GetSessionHandle()->GetFabricIndex(),
+                             ChipLogValueX64(apExchangeContext->GetSessionHandle()->AsSecureSession()->GetPeerNodeId()));
                 return Status::InvalidAction;
             }
 
@@ -400,12 +493,8 @@ Protocols::InteractionModel::Status InteractionModelEngine::OnReadInitialRequest
             }
         }
 
-        err = subscribeRequestParser.GetKeepSubscriptions(&keepExistingSubscriptions);
-        if (err != CHIP_NO_ERROR)
-        {
-            return Status::InvalidAction;
-        }
-
+        VerifyOrReturnError(subscribeRequestParser.GetKeepSubscriptions(&keepExistingSubscriptions) == CHIP_NO_ERROR,
+                            Status::InvalidAction);
         if (!keepExistingSubscriptions)
         {
             //
@@ -426,8 +515,7 @@ Protocols::InteractionModel::Status InteractionModelEngine::OnReadInitialRequest
             });
         }
     }
-
-    if (aInteractionType == ReadHandler::InteractionType::Read)
+    else
     {
         System::PacketBufferTLVReader reader;
         reader.Init(aPayload.Retain());
diff --git a/src/app/InteractionModelEngine.h b/src/app/InteractionModelEngine.h
index 0bd2c6738a3818..6475f8bceda547 100644
--- a/src/app/InteractionModelEngine.h
+++ b/src/app/InteractionModelEngine.h
@@ -378,6 +378,21 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
                                  System::PacketBufferHandle && aPayload) override;
     void OnResponseTimeout(Messaging::ExchangeContext * ec) override;
 
+    /**
+     * This parses the attribute path list to ensure it is well formed. If so, for each path in the list, it will expand to a list
+     * of concrete paths and walk each path to check if it has privileges to read that attribute.
+     *
+     * If there is AT LEAST one "existent path" (as the spec calls it) that has sufficient privilege, aHasValidAttributePath
+     * will be set to true. Otherwise, it will be set to false.
+     *
+     * aRequestedAttributePathCount will be updated to reflect the number of attribute paths in the request.
+     *
+     *
+     */
+    static CHIP_ERROR ParseAttributePaths(const Access::SubjectDescriptor & aSubjectDescriptor,
+                                          AttributePathIBs::Parser & aAttributePathListParser, bool & aHasValidAttributePath,
+                                          size_t & aRequestedAttributePathCount);
+
     /**
      * Called when Interaction Model receives a Read Request message.  Errors processing
      * the Read Request are handled entirely within this function.  If the
@@ -617,6 +632,13 @@ CHIP_ERROR ReadSingleClusterData(const Access::SubjectDescriptor & aSubjectDescr
                                  const ConcreteReadAttributePath & aPath, AttributeReportIBs::Builder & aAttributeReports,
                                  AttributeValueEncoder::AttributeEncodeState * apEncoderState);
 
+/**
+ *  Check whether concrete attribute path is an "existent attribute path" in spec terms.
+ *  @param[in]    aPath                 The concrete path of the data being read.
+ *  @retval  boolean
+ */
+bool ConcreteAttributePathExists(const ConcreteAttributePath & aPath);
+
 /**
  *  Get the registered attribute access override. nullptr when attribute access override is not found.
  *
diff --git a/src/app/MessageDef/AttributePathIB.cpp b/src/app/MessageDef/AttributePathIB.cpp
index c8d0fa51d29f4c..97d7f60568af0e 100644
--- a/src/app/MessageDef/AttributePathIB.cpp
+++ b/src/app/MessageDef/AttributePathIB.cpp
@@ -227,6 +227,63 @@ CHIP_ERROR AttributePathIB::Parser::GetListIndex(ConcreteDataAttributePath & aAt
     return err;
 }
 
+CHIP_ERROR AttributePathIB::Parser::ParsePath(AttributePathParams & aAttribute) const
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+
+    err = GetEndpoint(&(aAttribute.mEndpointId));
+    if (err == CHIP_NO_ERROR)
+    {
+        VerifyOrReturnError(!aAttribute.HasWildcardEndpointId(), CHIP_IM_GLOBAL_STATUS(InvalidAction));
+    }
+    else if (err == CHIP_END_OF_TLV)
+    {
+        err = CHIP_NO_ERROR;
+    }
+    VerifyOrReturnError(err == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(InvalidAction));
+
+    err = GetCluster(&aAttribute.mClusterId);
+    if (err == CHIP_NO_ERROR)
+    {
+        VerifyOrReturnError(IsValidClusterId(aAttribute.mClusterId), CHIP_IM_GLOBAL_STATUS(InvalidAction));
+    }
+    else if (err == CHIP_END_OF_TLV)
+    {
+        err = CHIP_NO_ERROR;
+    }
+    VerifyOrReturnError(err == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(InvalidAction));
+
+    err = GetAttribute(&aAttribute.mAttributeId);
+    if (err == CHIP_NO_ERROR)
+    {
+        VerifyOrReturnError(IsValidAttributeId(aAttribute.mAttributeId), CHIP_IM_GLOBAL_STATUS(InvalidAction));
+    }
+    else if (err == CHIP_END_OF_TLV)
+    {
+        err = CHIP_NO_ERROR;
+    }
+    VerifyOrReturnError(err == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(InvalidAction));
+
+    // A wildcard cluster requires that the attribute path either be
+    // wildcard or a global attribute.
+    VerifyOrReturnError(!aAttribute.HasWildcardClusterId() || aAttribute.HasWildcardAttributeId() ||
+                            IsGlobalAttribute(aAttribute.mAttributeId),
+                        CHIP_IM_GLOBAL_STATUS(InvalidAction));
+
+    err = GetListIndex(&aAttribute.mListIndex);
+    if (err == CHIP_NO_ERROR)
+    {
+        VerifyOrReturnError(!aAttribute.HasWildcardAttributeId() && !aAttribute.HasWildcardListIndex(),
+                            CHIP_IM_GLOBAL_STATUS(InvalidAction));
+    }
+    else if (err == CHIP_END_OF_TLV)
+    {
+        err = CHIP_NO_ERROR;
+    }
+    VerifyOrReturnError(err == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(InvalidAction));
+    return CHIP_NO_ERROR;
+}
+
 AttributePathIB::Builder & AttributePathIB::Builder::EnableTagCompression(const bool aEnableTagCompression)
 {
     // skip if error has already been set
diff --git a/src/app/MessageDef/AttributePathIB.h b/src/app/MessageDef/AttributePathIB.h
index e5192ccb119739..81b1a33e58a904 100644
--- a/src/app/MessageDef/AttributePathIB.h
+++ b/src/app/MessageDef/AttributePathIB.h
@@ -153,6 +153,14 @@ class Parser : public ListParser
     CHIP_ERROR GetListIndex(ConcreteDataAttributePath & aAttributePath) const;
 
     // TODO(#14934) Add a function to get ConcreteDataAttributePath from AttributePathIB::Parser directly.
+
+    /**
+     * @brief Parse the attribute path into an AttributePathParams object. As part of parsing,
+     * validity checks for each path item will be done as well.
+     *
+     * If any errors are encountered, an IM error of 'InvalidAction' will be returned.
+     */
+    CHIP_ERROR ParsePath(AttributePathParams & attribute) const;
 };
 
 class Builder : public ListBuilder
diff --git a/src/app/MessageDef/EventPathIB.cpp b/src/app/MessageDef/EventPathIB.cpp
index 35c5b098b94c55..84c9f6680bf260 100644
--- a/src/app/MessageDef/EventPathIB.cpp
+++ b/src/app/MessageDef/EventPathIB.cpp
@@ -26,6 +26,8 @@
 
 #include <app/AppConfig.h>
 
+#include <protocols/interaction_model/Constants.h>
+
 namespace chip {
 namespace app {
 #if CHIP_CONFIG_IM_ENABLE_SCHEMA_CHECK
@@ -171,6 +173,50 @@ CHIP_ERROR EventPathIB::Parser::GetIsUrgent(bool * const apIsUrgent) const
     return GetSimpleValue(to_underlying(Tag::kIsUrgent), TLV::kTLVType_Boolean, apIsUrgent);
 }
 
+CHIP_ERROR EventPathIB::Parser::ParsePath(EventPathParams & aEvent) const
+{
+    CHIP_ERROR err = GetEndpoint(&(aEvent.mEndpointId));
+    if (err == CHIP_NO_ERROR)
+    {
+        VerifyOrReturnError(!aEvent.HasWildcardEndpointId(), CHIP_IM_GLOBAL_STATUS(InvalidAction));
+    }
+    else if (err == CHIP_END_OF_TLV)
+    {
+        err = CHIP_NO_ERROR;
+    }
+    VerifyOrReturnError(err == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(InvalidAction));
+
+    err = GetCluster(&(aEvent.mClusterId));
+    if (err == CHIP_NO_ERROR)
+    {
+        VerifyOrReturnError(!aEvent.HasWildcardClusterId(), CHIP_IM_GLOBAL_STATUS(InvalidAction));
+    }
+    else if (err == CHIP_END_OF_TLV)
+    {
+        err = CHIP_NO_ERROR;
+    }
+    VerifyOrReturnError(err == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(InvalidAction));
+
+    err = GetEvent(&(aEvent.mEventId));
+    if (CHIP_END_OF_TLV == err)
+    {
+        err = CHIP_NO_ERROR;
+    }
+    else if (err == CHIP_NO_ERROR)
+    {
+        VerifyOrReturnError(!aEvent.HasWildcardEventId(), CHIP_IM_GLOBAL_STATUS(InvalidAction));
+    }
+    VerifyOrReturnError(err == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(InvalidAction));
+
+    err = GetIsUrgent(&(aEvent.mIsUrgentEvent));
+    if (CHIP_END_OF_TLV == err)
+    {
+        err = CHIP_NO_ERROR;
+    }
+    VerifyOrReturnError(err == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(InvalidAction));
+    return CHIP_NO_ERROR;
+}
+
 EventPathIB::Builder & EventPathIB::Builder::Node(const NodeId aNode)
 {
     // skip if error has already been set
diff --git a/src/app/MessageDef/EventPathIB.h b/src/app/MessageDef/EventPathIB.h
index 32b7948dfc6d54..0ad2000938ac86 100644
--- a/src/app/MessageDef/EventPathIB.h
+++ b/src/app/MessageDef/EventPathIB.h
@@ -127,6 +127,14 @@ class Parser : public ListParser
      *          #CHIP_ERROR_IM_MALFORMED_EVENT_PATH_IB if the path from the reader is not a valid concrere event path.
      */
     CHIP_ERROR GetEventPath(ConcreteEventPath * const apPath) const;
+
+    /**
+     * @brief Parse the event path into an EventPathParams object. As part of parsing,
+     * validity checks for each path item will be done as well.
+     *
+     * If any errors are encountered, an IM error of 'InvalidAction' will be returned.
+     */
+    CHIP_ERROR ParsePath(EventPathParams & aEvent) const;
 };
 
 class Builder : public ListBuilder
diff --git a/src/app/ReadHandler.cpp b/src/app/ReadHandler.cpp
index 913dcce726ca10..64bcbb1a7e516b 100644
--- a/src/app/ReadHandler.cpp
+++ b/src/app/ReadHandler.cpp
@@ -315,7 +315,7 @@ CHIP_ERROR ReadHandler::ProcessReadRequest(System::PacketBufferHandle && aPayloa
     }
     else if (err == CHIP_NO_ERROR)
     {
-        ReturnErrorOnFailure(ProcessAttributePathList(attributePathListParser));
+        ReturnErrorOnFailure(ProcessAttributePaths(attributePathListParser));
         DataVersionFilterIBs::Parser dataVersionFilterListParser;
         err = readRequestParser.GetDataVersionFilters(&dataVersionFilterListParser);
         if (err == CHIP_END_OF_TLV)
@@ -363,75 +363,19 @@ CHIP_ERROR ReadHandler::ProcessReadRequest(System::PacketBufferHandle && aPayloa
     return CHIP_NO_ERROR;
 }
 
-CHIP_ERROR ReadHandler::ProcessAttributePathList(AttributePathIBs::Parser & aAttributePathListParser)
+CHIP_ERROR ReadHandler::ProcessAttributePaths(AttributePathIBs::Parser & aAttributePathListParser)
 {
     CHIP_ERROR err = CHIP_NO_ERROR;
     TLV::TLVReader reader;
     aAttributePathListParser.GetReader(&reader);
     while (CHIP_NO_ERROR == (err = reader.Next()))
     {
-        VerifyOrExit(TLV::AnonymousTag() == reader.GetTag(), err = CHIP_ERROR_INVALID_TLV_TAG);
+        VerifyOrReturnError(TLV::AnonymousTag() == reader.GetTag(), CHIP_ERROR_INVALID_TLV_TAG);
         AttributePathParams attribute;
         AttributePathIB::Parser path;
-        err = path.Init(reader);
-        SuccessOrExit(err);
-
-        err = path.GetEndpoint(&(attribute.mEndpointId));
-        if (err == CHIP_NO_ERROR)
-        {
-            VerifyOrExit(!attribute.HasWildcardEndpointId(), err = CHIP_IM_GLOBAL_STATUS(InvalidAction));
-        }
-        else if (err == CHIP_END_OF_TLV)
-        {
-            err = CHIP_NO_ERROR;
-        }
-        SuccessOrExit(err);
-
-        ClusterId clusterId = kInvalidClusterId;
-        err                 = path.GetCluster(&clusterId);
-        if (err == CHIP_NO_ERROR)
-        {
-            VerifyOrExit(IsValidClusterId(clusterId), err = CHIP_IM_GLOBAL_STATUS(InvalidAction));
-            attribute.mClusterId = clusterId;
-        }
-        else if (err == CHIP_END_OF_TLV)
-        {
-            err = CHIP_NO_ERROR;
-        }
-        SuccessOrExit(err);
-
-        AttributeId attributeId = kInvalidAttributeId;
-        err                     = path.GetAttribute(&attributeId);
-        if (CHIP_END_OF_TLV == err)
-        {
-            err = CHIP_NO_ERROR;
-        }
-        else if (err == CHIP_NO_ERROR)
-        {
-            VerifyOrExit(IsValidAttributeId(attributeId), err = CHIP_IM_GLOBAL_STATUS(InvalidAction));
-            attribute.mAttributeId = attributeId;
-        }
-        SuccessOrExit(err);
-
-        // A wildcard cluster requires that the attribute path either be
-        // wildcard or a global attribute.
-        VerifyOrExit(!attribute.HasWildcardClusterId() || attribute.HasWildcardAttributeId() ||
-                         IsGlobalAttribute(attribute.mAttributeId),
-                     err = CHIP_IM_GLOBAL_STATUS(InvalidAction));
-
-        err = path.GetListIndex(&(attribute.mListIndex));
-        if (CHIP_NO_ERROR == err)
-        {
-            VerifyOrExit(!attribute.HasWildcardAttributeId() && !attribute.HasWildcardListIndex(),
-                         err = CHIP_IM_GLOBAL_STATUS(InvalidAction));
-        }
-        else if (CHIP_END_OF_TLV == err)
-        {
-            err = CHIP_NO_ERROR;
-        }
-        SuccessOrExit(err);
-        err = InteractionModelEngine::GetInstance()->PushFrontAttributePathList(mpAttributePathList, attribute);
-        SuccessOrExit(err);
+        ReturnErrorOnFailure(path.Init(reader));
+        ReturnErrorOnFailure(path.ParsePath(attribute));
+        ReturnErrorOnFailure(InteractionModelEngine::GetInstance()->PushFrontAttributePathList(mpAttributePathList, attribute));
     }
     // if we have exhausted this container
     if (CHIP_END_OF_TLV == err)
@@ -440,8 +384,6 @@ CHIP_ERROR ReadHandler::ProcessAttributePathList(AttributePathIBs::Parser & aAtt
         mAttributePathExpandIterator = AttributePathExpandIterator(mpAttributePathList);
         err                          = CHIP_NO_ERROR;
     }
-
-exit:
     return err;
 }
 
@@ -487,47 +429,7 @@ CHIP_ERROR ReadHandler::ProcessEventPaths(EventPathIBs::Parser & aEventPathsPars
         EventPathParams event;
         EventPathIB::Parser path;
         ReturnErrorOnFailure(path.Init(reader));
-
-        err = path.GetEndpoint(&(event.mEndpointId));
-        if (err == CHIP_NO_ERROR)
-        {
-            VerifyOrReturnError(!event.HasWildcardEndpointId(), err = CHIP_ERROR_IM_MALFORMED_EVENT_PATH_IB);
-        }
-        else if (err == CHIP_END_OF_TLV)
-        {
-            err = CHIP_NO_ERROR;
-        }
-        ReturnErrorOnFailure(err);
-
-        err = path.GetCluster(&(event.mClusterId));
-        if (err == CHIP_NO_ERROR)
-        {
-            VerifyOrReturnError(!event.HasWildcardClusterId(), err = CHIP_ERROR_IM_MALFORMED_EVENT_PATH_IB);
-        }
-        else if (err == CHIP_END_OF_TLV)
-        {
-            err = CHIP_NO_ERROR;
-        }
-        ReturnErrorOnFailure(err);
-
-        err = path.GetEvent(&(event.mEventId));
-        if (CHIP_END_OF_TLV == err)
-        {
-            err = CHIP_NO_ERROR;
-        }
-        else if (err == CHIP_NO_ERROR)
-        {
-            VerifyOrReturnError(!event.HasWildcardEventId(), err = CHIP_ERROR_IM_MALFORMED_EVENT_PATH_IB);
-        }
-        ReturnErrorOnFailure(err);
-
-        err = path.GetIsUrgent(&(event.mIsUrgentEvent));
-        if (CHIP_END_OF_TLV == err)
-        {
-            err = CHIP_NO_ERROR;
-        }
-        ReturnErrorOnFailure(err);
-
+        ReturnErrorOnFailure(path.ParsePath(event));
         ReturnErrorOnFailure(InteractionModelEngine::GetInstance()->PushFrontEventPathParamsList(mpEventPathList, event));
     }
 
@@ -667,7 +569,7 @@ CHIP_ERROR ReadHandler::ProcessSubscribeRequest(System::PacketBufferHandle && aP
     }
     else if (err == CHIP_NO_ERROR)
     {
-        ReturnErrorOnFailure(ProcessAttributePathList(attributePathListParser));
+        ReturnErrorOnFailure(ProcessAttributePaths(attributePathListParser));
         DataVersionFilterIBs::Parser dataVersionFilterListParser;
         err = subscribeRequestParser.GetDataVersionFilters(&dataVersionFilterListParser);
         if (err == CHIP_END_OF_TLV)
diff --git a/src/app/ReadHandler.h b/src/app/ReadHandler.h
index b07ef01b2c956a..7e32fa920a2636 100644
--- a/src/app/ReadHandler.h
+++ b/src/app/ReadHandler.h
@@ -362,7 +362,7 @@ class ReadHandler : public Messaging::ExchangeDelegate
     CHIP_ERROR SendSubscribeResponse();
     CHIP_ERROR ProcessSubscribeRequest(System::PacketBufferHandle && aPayload);
     CHIP_ERROR ProcessReadRequest(System::PacketBufferHandle && aPayload);
-    CHIP_ERROR ProcessAttributePathList(AttributePathIBs::Parser & aAttributePathListParser);
+    CHIP_ERROR ProcessAttributePaths(AttributePathIBs::Parser & aAttributePathListParser);
     CHIP_ERROR ProcessEventPaths(EventPathIBs::Parser & aEventPathsParser);
     CHIP_ERROR ProcessEventFilters(EventFilterIBs::Parser & aEventFiltersParser);
     CHIP_ERROR OnStatusResponse(Messaging::ExchangeContext * apExchangeContext, System::PacketBufferHandle && aPayload,
diff --git a/src/app/tests/BUILD.gn b/src/app/tests/BUILD.gn
index 7d57e72ebb8667..d11f29a66c72c2 100644
--- a/src/app/tests/BUILD.gn
+++ b/src/app/tests/BUILD.gn
@@ -102,6 +102,8 @@ chip_test_suite("tests") {
     test_sources += [ "TestFailSafeContext.cpp" ]
   }
 
+  test_sources += [ "TestAclAttribute.cpp" ]
+
   #
   # On NRF platforms, the allocation of a large number of pbufs in this test
   # to exercise chunking causes it to run out of memory. For now, disable it there.
diff --git a/src/app/tests/TestAclAttribute.cpp b/src/app/tests/TestAclAttribute.cpp
new file mode 100644
index 00000000000000..ca51e96401ebdc
--- /dev/null
+++ b/src/app/tests/TestAclAttribute.cpp
@@ -0,0 +1,283 @@
+/*
+ *
+ *    Copyright (c) 2021 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed 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.
+ */
+
+#include "lib/support/CHIPMem.h"
+#include <access/examples/PermissiveAccessControlDelegate.h>
+#include <app/AttributeAccessInterface.h>
+#include <app/InteractionModelEngine.h>
+#include <app/MessageDef/AttributeReportIBs.h>
+#include <app/MessageDef/EventDataIB.h>
+#include <app/tests/AppTestContext.h>
+#include <app/util/basic-types.h>
+#include <app/util/mock/Constants.h>
+#include <app/util/mock/Functions.h>
+#include <lib/core/CHIPCore.h>
+#include <lib/core/CHIPTLV.h>
+#include <lib/core/CHIPTLVDebug.hpp>
+#include <lib/core/CHIPTLVUtilities.hpp>
+#include <lib/support/CHIPCounter.h>
+#include <lib/support/ErrorStr.h>
+#include <lib/support/UnitTestContext.h>
+#include <lib/support/UnitTestRegistration.h>
+#include <messaging/ExchangeContext.h>
+#include <messaging/Flags.h>
+#include <nlunit-test.h>
+#include <protocols/interaction_model/Constants.h>
+
+#include <type_traits>
+
+namespace {
+using namespace chip;
+using namespace chip::Access;
+
+chip::ClusterId kTestClusterId        = 1;
+chip::ClusterId kTestDeniedClusterId1 = 1000;
+chip::ClusterId kTestDeniedClusterId2 = 3;
+chip::EndpointId kTestEndpointId      = 4;
+
+class TestAccessControlDelegate : public AccessControl::Delegate
+{
+public:
+    CHIP_ERROR Check(const SubjectDescriptor & subjectDescriptor, const chip::Access::RequestPath & requestPath,
+                     Privilege requestPrivilege) override
+    {
+        if (requestPath.cluster == kTestDeniedClusterId2)
+        {
+            return CHIP_ERROR_ACCESS_DENIED;
+        }
+        return CHIP_NO_ERROR;
+    }
+};
+
+AccessControl::Delegate * GetTestAccessControlDelegate()
+{
+    static TestAccessControlDelegate accessControlDelegate;
+    return &accessControlDelegate;
+}
+
+class TestDeviceTypeResolver : public AccessControl::DeviceTypeResolver
+{
+public:
+    bool IsDeviceTypeOnEndpoint(DeviceTypeId deviceType, EndpointId endpoint) override { return false; }
+} gDeviceTypeResolver;
+
+class TestAccessContext : public chip::Test::AppContext
+{
+public:
+    static int Initialize(void * context)
+    {
+        if (AppContext::Initialize(context) != SUCCESS)
+            return FAILURE;
+        Access::GetAccessControl().Finish();
+        Access::GetAccessControl().Init(GetTestAccessControlDelegate(), gDeviceTypeResolver);
+        return SUCCESS;
+    }
+
+    static int Finalize(void * context)
+    {
+        if (AppContext::Finalize(context) != SUCCESS)
+            return FAILURE;
+        return SUCCESS;
+    }
+
+private:
+    chip::MonotonicallyIncreasingCounter<chip::EventNumber> mEventCounter;
+};
+
+class MockInteractionModelApp : public chip::app::ReadClient::Callback
+{
+public:
+    void OnAttributeData(const chip::app::ConcreteDataAttributePath & aPath, chip::TLV::TLVReader * apData,
+                         const chip::app::StatusIB & status) override
+    {
+        mGotReport          = true;
+        mLastStatusReceived = status;
+    }
+
+    void OnError(CHIP_ERROR aError) override { mError = aError; }
+
+    void OnDone(chip::app::ReadClient *) override {}
+
+    void OnDeallocatePaths(chip::app::ReadPrepareParams && aReadPrepareParams) override
+    {
+        if (aReadPrepareParams.mpAttributePathParamsList != nullptr)
+        {
+            delete[] aReadPrepareParams.mpAttributePathParamsList;
+        }
+
+        if (aReadPrepareParams.mpDataVersionFilterList != nullptr)
+        {
+            delete[] aReadPrepareParams.mpDataVersionFilterList;
+        }
+    }
+
+    bool mGotReport = false;
+    chip::app::StatusIB mLastStatusReceived;
+    CHIP_ERROR mError = CHIP_NO_ERROR;
+};
+} // namespace
+
+namespace chip {
+namespace app {
+
+bool ConcreteAttributePathExists(const ConcreteAttributePath & aPath)
+{
+    return aPath.mClusterId != kTestDeniedClusterId1;
+}
+
+class TestAclAttribute
+{
+public:
+    static void TestACLDeniedAttribute(nlTestSuite * apSuite, void * apContext);
+};
+
+// Read Client sends a malformed subscribe request, interaction model engine fails to parse the request and generates a status
+// report to client, and client is closed.
+void TestAclAttribute::TestACLDeniedAttribute(nlTestSuite * apSuite, void * apContext)
+{
+    TestAccessContext & ctx = *static_cast<TestAccessContext *>(apContext);
+    CHIP_ERROR err          = CHIP_NO_ERROR;
+
+    Messaging::ReliableMessageMgr * rm = ctx.GetExchangeManager().GetReliableMessageMgr();
+    NL_TEST_ASSERT(apSuite, rm->TestGetCountRetransTable() == 0);
+
+    MockInteractionModelApp delegate;
+    auto * engine = chip::app::InteractionModelEngine::GetInstance();
+    err           = engine->Init(&ctx.GetExchangeManager(), &ctx.GetFabricTable());
+    NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+
+    {
+        app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), delegate,
+                                   chip::app::ReadClient::InteractionType::Subscribe);
+
+        chip::app::AttributePathParams attributePathParams[2];
+        attributePathParams[0].mEndpointId  = kTestEndpointId;
+        attributePathParams[0].mClusterId   = kTestDeniedClusterId1;
+        attributePathParams[0].mAttributeId = 1;
+
+        attributePathParams[1].mEndpointId  = kTestEndpointId;
+        attributePathParams[1].mClusterId   = kTestDeniedClusterId1;
+        attributePathParams[1].mAttributeId = 2;
+
+        ReadPrepareParams readPrepareParams(ctx.GetSessionBobToAlice());
+        readPrepareParams.mpAttributePathParamsList    = attributePathParams;
+        readPrepareParams.mAttributePathParamsListSize = 2;
+
+        err = readClient.SendRequest(readPrepareParams);
+        NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+
+        ctx.DrainAndServiceIO();
+        NL_TEST_ASSERT(apSuite, delegate.mError == CHIP_IM_GLOBAL_STATUS(InvalidAction));
+        NL_TEST_ASSERT(apSuite, !delegate.mGotReport);
+        delegate.mError     = CHIP_NO_ERROR;
+        delegate.mGotReport = false;
+    }
+
+    {
+        app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), delegate,
+                                   chip::app::ReadClient::InteractionType::Subscribe);
+
+        chip::app::AttributePathParams attributePathParams[2];
+
+        attributePathParams[0].mClusterId   = kTestDeniedClusterId2;
+        attributePathParams[0].mAttributeId = 1;
+
+        attributePathParams[1].mClusterId   = kTestDeniedClusterId2;
+        attributePathParams[1].mAttributeId = 2;
+
+        ReadPrepareParams readPrepareParams(ctx.GetSessionBobToAlice());
+        readPrepareParams.mpAttributePathParamsList    = attributePathParams;
+        readPrepareParams.mAttributePathParamsListSize = 2;
+
+        err = readClient.SendRequest(readPrepareParams);
+        NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+
+        ctx.DrainAndServiceIO();
+        NL_TEST_ASSERT(apSuite, delegate.mError == CHIP_IM_GLOBAL_STATUS(InvalidAction));
+        NL_TEST_ASSERT(apSuite, !delegate.mGotReport);
+        delegate.mError     = CHIP_NO_ERROR;
+        delegate.mGotReport = false;
+    }
+
+    {
+        app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), delegate,
+                                   chip::app::ReadClient::InteractionType::Subscribe);
+
+        chip::app::AttributePathParams attributePathParams[2];
+        attributePathParams[0].mEndpointId  = kTestEndpointId;
+        attributePathParams[0].mClusterId   = kTestDeniedClusterId1;
+        attributePathParams[0].mAttributeId = 1;
+
+        attributePathParams[1].mEndpointId  = kTestEndpointId;
+        attributePathParams[1].mClusterId   = kTestClusterId;
+        attributePathParams[1].mAttributeId = 2;
+
+        ReadPrepareParams readPrepareParams(ctx.GetSessionBobToAlice());
+        readPrepareParams.mpAttributePathParamsList    = attributePathParams;
+        readPrepareParams.mAttributePathParamsListSize = 2;
+
+        err = readClient.SendRequest(readPrepareParams);
+        NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+
+        ctx.DrainAndServiceIO();
+        NL_TEST_ASSERT(apSuite, delegate.mError == CHIP_NO_ERROR);
+        NL_TEST_ASSERT(apSuite, delegate.mGotReport);
+        NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadHandlers(ReadHandler::InteractionType::Subscribe) == 1);
+        delegate.mError     = CHIP_NO_ERROR;
+        delegate.mGotReport = false;
+    }
+
+    NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadClients() == 0);
+    engine->Shutdown();
+    NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
+}
+} // namespace app
+} // namespace chip
+
+namespace {
+
+/**
+ *   Test Suite. It lists all the test functions.
+ */
+
+// clang-format off
+const nlTest sTests[] =
+{
+    NL_TEST_DEF("TestACLDeniedAttribute", chip::app::TestAclAttribute::TestACLDeniedAttribute),
+    NL_TEST_SENTINEL()
+};
+// clang-format on
+
+// clang-format off
+nlTestSuite sSuite =
+{
+    "TestAclAttribute",
+    &sTests[0],
+    TestAccessContext::Initialize,
+    TestAccessContext::Finalize
+};
+// clang-format on
+
+} // namespace
+
+int TestAclAttribute()
+{
+    return chip::ExecuteTestsWithContext<TestAccessContext>(&sSuite);
+}
+
+CHIP_REGISTER_TEST_SUITE(TestAclAttribute)
diff --git a/src/app/tests/TestAclEvent.cpp b/src/app/tests/TestAclEvent.cpp
index b67ca621c7e44f..1238e7c7142ee3 100644
--- a/src/app/tests/TestAclEvent.cpp
+++ b/src/app/tests/TestAclEvent.cpp
@@ -16,12 +16,6 @@
  *    limitations under the License.
  */
 
-/**
- *    @file
- *      This file implements unit tests for CHIP Interaction Model Read Interaction
- *
- */
-
 #include "lib/support/CHIPMem.h"
 #include <access/AccessControl.h>
 #include <app/AttributeAccessInterface.h>
@@ -55,7 +49,7 @@ uint8_t gDebugEventBuffer[128];
 uint8_t gInfoEventBuffer[128];
 uint8_t gCritEventBuffer[128];
 chip::app::CircularEventBuffer gCircularEventBuffer[3];
-chip::ClusterId kTestClusterId1    = 6;
+chip::ClusterId kTestClusterId1    = 100;
 chip::ClusterId kTestClusterId2    = 7;
 chip::EndpointId kTestEndpointId   = 1;
 chip::EventId kTestEventIdDebug    = 1;
diff --git a/src/app/tests/TestReadInteraction.cpp b/src/app/tests/TestReadInteraction.cpp
index e0527b9e4c66ff..44ba49c400a4b9 100644
--- a/src/app/tests/TestReadInteraction.cpp
+++ b/src/app/tests/TestReadInteraction.cpp
@@ -338,6 +338,7 @@ class TestReadInteraction
     static void TestReadHandlerInvalidSubscribeRequest(nlTestSuite * apSuite, void * apContext);
     static void TestSubscribeInvalidateFabric(nlTestSuite * apSuite, void * apContext);
     static void TestShutdownSubscription(nlTestSuite * apSuite, void * apContext);
+    static void TestReadHandlerMalformedSubscribeRequest(nlTestSuite * apSuite, void * apContext);
 
 private:
     static void GenerateReportData(nlTestSuite * apSuite, void * apContext, System::PacketBufferHandle & aPayload,
@@ -3521,6 +3522,51 @@ void TestReadInteraction::TestReadChunkingInvalidSubscriptionId(nlTestSuite * ap
     ctx.CreateSessionBobToAlice();
 }
 
+// Read Client sends a malformed subscribe request, interaction model engine fails to parse the request and generates a status
+// report to client, and client is closed.
+void TestReadInteraction::TestReadHandlerMalformedSubscribeRequest(nlTestSuite * apSuite, void * apContext)
+{
+    TestContext & ctx = *static_cast<TestContext *>(apContext);
+    CHIP_ERROR err    = CHIP_NO_ERROR;
+
+    Messaging::ReliableMessageMgr * rm = ctx.GetExchangeManager().GetReliableMessageMgr();
+    // Shouldn't have anything in the retransmit table when starting the test.
+    NL_TEST_ASSERT(apSuite, rm->TestGetCountRetransTable() == 0);
+
+    MockInteractionModelApp delegate;
+    auto * engine = chip::app::InteractionModelEngine::GetInstance();
+    err           = engine->Init(&ctx.GetExchangeManager(), &ctx.GetFabricTable());
+    NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+
+    ReadPrepareParams readPrepareParams(ctx.GetSessionBobToAlice());
+
+    {
+        app::ReadClient readClient(chip::app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), delegate,
+                                   chip::app::ReadClient::InteractionType::Subscribe);
+        System::PacketBufferHandle msgBuf;
+        ReadRequestMessage::Builder request;
+        System::PacketBufferTLVWriter writer;
+
+        chip::app::InitWriterWithSpaceReserved(writer, 0);
+        err = request.Init(&writer);
+        NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+        err = writer.Finalize(&msgBuf);
+        NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+        auto exchange = readClient.mpExchangeMgr->NewContext(readPrepareParams.mSessionHolder.Get().Value(), &readClient);
+        NL_TEST_ASSERT(apSuite, exchange != nullptr);
+        readClient.mExchange.Grab(exchange);
+        readClient.MoveToState(app::ReadClient::ClientState::AwaitingInitialReport);
+        err = readClient.mExchange->SendMessage(Protocols::InteractionModel::MsgType::ReadRequest, std::move(msgBuf),
+                                                Messaging::SendFlags(Messaging::SendMessageFlags::kExpectResponse));
+        NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+        ctx.DrainAndServiceIO();
+        NL_TEST_ASSERT(apSuite, delegate.mError == CHIP_IM_GLOBAL_STATUS(InvalidAction));
+    }
+    NL_TEST_ASSERT(apSuite, engine->GetNumActiveReadClients() == 0);
+    engine->Shutdown();
+    NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
+}
+
 // Read Client sends a malformed read request, interaction model engine fails to parse the request and generates a status report to
 // client, and client is closed.
 void TestReadInteraction::TestReadHandlerMalformedReadRequest1(nlTestSuite * apSuite, void * apContext)
@@ -3953,6 +3999,7 @@ const nlTest sTests[] =
     NL_TEST_DEF("TestReadChunkingInvalidSubscriptionId", chip::app::TestReadInteraction::TestReadChunkingInvalidSubscriptionId),
     NL_TEST_DEF("TestReadHandlerMalformedReadRequest1", chip::app::TestReadInteraction::TestReadHandlerMalformedReadRequest1),
     NL_TEST_DEF("TestReadHandlerMalformedReadRequest2", chip::app::TestReadInteraction::TestReadHandlerMalformedReadRequest2),
+    NL_TEST_DEF("TestReadHandlerMalformedSubscribeRequest", chip::app::TestReadInteraction::TestReadHandlerMalformedSubscribeRequest),
     NL_TEST_DEF("TestSubscribeSendUnknownMessage", chip::app::TestReadInteraction::TestSubscribeSendUnknownMessage),
     NL_TEST_DEF("TestSubscribeSendInvalidStatusReport", chip::app::TestReadInteraction::TestSubscribeSendInvalidStatusReport),
     NL_TEST_DEF("TestReadHandlerInvalidSubscribeRequest", chip::app::TestReadInteraction::TestReadHandlerInvalidSubscribeRequest),
diff --git a/src/app/tests/integration/chip_im_initiator.cpp b/src/app/tests/integration/chip_im_initiator.cpp
index 4636e14bc3fc0f..5457bbb3fd79ef 100644
--- a/src/app/tests/integration/chip_im_initiator.cpp
+++ b/src/app/tests/integration/chip_im_initiator.cpp
@@ -654,6 +654,11 @@ const EmberAfAttributeMetadata * GetAttributeMetadata(const ConcreteAttributePat
     return &stub;
 }
 
+bool ConcreteAttributePathExists(const ConcreteAttributePath & aPath)
+{
+    return true;
+}
+
 CHIP_ERROR WriteSingleClusterData(const Access::SubjectDescriptor & aSubjectDescriptor, const ConcreteDataAttributePath & aPath,
                                   TLV::TLVReader & aReader, WriteHandler *)
 {
diff --git a/src/app/tests/integration/chip_im_responder.cpp b/src/app/tests/integration/chip_im_responder.cpp
index 9952256c4d479e..34978611e4f3bf 100644
--- a/src/app/tests/integration/chip_im_responder.cpp
+++ b/src/app/tests/integration/chip_im_responder.cpp
@@ -122,6 +122,11 @@ CHIP_ERROR ReadSingleClusterData(const Access::SubjectDescriptor & aSubjectDescr
     return CHIP_NO_ERROR;
 }
 
+bool ConcreteAttributePathExists(const ConcreteAttributePath & aPath)
+{
+    return true;
+}
+
 const EmberAfAttributeMetadata * GetAttributeMetadata(const ConcreteAttributePath & aConcreteClusterPath)
 {
     // Note: This test does not make use of the real attribute metadata.
diff --git a/src/app/util/ember-compatibility-functions.cpp b/src/app/util/ember-compatibility-functions.cpp
index d90c97a50b6f8e..3ac3b3258c41bc 100644
--- a/src/app/util/ember-compatibility-functions.cpp
+++ b/src/app/util/ember-compatibility-functions.cpp
@@ -516,6 +516,18 @@ Protocols::InteractionModel::Status UnsupportedAttributeStatus(const ConcreteAtt
 
 } // anonymous namespace
 
+bool ConcreteAttributePathExists(const ConcreteAttributePath & aPath)
+{
+    for (auto & attr : GlobalAttributesNotInMetadata)
+    {
+        if (attr == aPath.mAttributeId)
+        {
+            return (emberAfFindCluster(aPath.mEndpointId, aPath.mClusterId, CLUSTER_MASK_SERVER) != nullptr);
+        }
+    }
+    return (emberAfLocateAttributeMetadata(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId) != nullptr);
+}
+
 CHIP_ERROR ReadSingleClusterData(const SubjectDescriptor & aSubjectDescriptor, bool aIsFabricFiltered,
                                  const ConcreteReadAttributePath & aPath, AttributeReportIBs::Builder & aAttributeReports,
                                  AttributeValueEncoder::AttributeEncodeState * apEncoderState)
diff --git a/src/controller/tests/TestReadChunking.cpp b/src/controller/tests/TestReadChunking.cpp
index 5dc6006edc09ba..1e1f0c41fd6e7d 100644
--- a/src/controller/tests/TestReadChunking.cpp
+++ b/src/controller/tests/TestReadChunking.cpp
@@ -68,10 +68,10 @@ constexpr AttributeId kTestListAttribute = 6;
 constexpr AttributeId kTestBadAttribute =
     7; // Reading this attribute will return CHIP_ERROR_NO_MEMORY but nothing is actually encoded.
 
-class TestCommandInteraction
+class TestReadChunking
 {
 public:
-    TestCommandInteraction() {}
+    TestReadChunking() {}
     static void TestChunking(nlTestSuite * apSuite, void * apContext);
     static void TestListChunking(nlTestSuite * apSuite, void * apContext);
     static void TestBadChunking(nlTestSuite * apSuite, void * apContext);
@@ -359,7 +359,7 @@ void TestMutableReadCallback::OnAttributeData(const app::ConcreteDataAttributePa
  * as we can possibly cover.
  *
  */
-void TestCommandInteraction::TestChunking(nlTestSuite * apSuite, void * apContext)
+void TestReadChunking::TestChunking(nlTestSuite * apSuite, void * apContext)
 {
     TestContext & ctx                    = *static_cast<TestContext *>(apContext);
     auto sessionHandle                   = ctx.GetSessionBobToAlice();
@@ -424,7 +424,7 @@ void TestCommandInteraction::TestChunking(nlTestSuite * apSuite, void * apContex
 }
 
 // Similar to the test above, but for the list chunking feature.
-void TestCommandInteraction::TestListChunking(nlTestSuite * apSuite, void * apContext)
+void TestReadChunking::TestListChunking(nlTestSuite * apSuite, void * apContext)
 {
     TestContext & ctx                    = *static_cast<TestContext *>(apContext);
     auto sessionHandle                   = ctx.GetSessionBobToAlice();
@@ -488,7 +488,7 @@ void TestCommandInteraction::TestListChunking(nlTestSuite * apSuite, void * apCo
 }
 
 // Read an attribute that can never fit into the buffer. Result in an empty report, server should shutdown the transaction.
-void TestCommandInteraction::TestBadChunking(nlTestSuite * apSuite, void * apContext)
+void TestReadChunking::TestBadChunking(nlTestSuite * apSuite, void * apContext)
 {
     TestContext & ctx                    = *static_cast<TestContext *>(apContext);
     auto sessionHandle                   = ctx.GetSessionBobToAlice();
@@ -540,7 +540,7 @@ void TestCommandInteraction::TestBadChunking(nlTestSuite * apSuite, void * apCon
 /*
  * This test contains two parts, one is to enable a new endpoint on the fly, another is to disable it and re-enable it.
  */
-void TestCommandInteraction::TestDynamicEndpoint(nlTestSuite * apSuite, void * apContext)
+void TestReadChunking::TestDynamicEndpoint(nlTestSuite * apSuite, void * apContext)
 {
     TestContext & ctx                    = *static_cast<TestContext *>(apContext);
     auto sessionHandle                   = ctx.GetSessionBobToAlice();
@@ -565,16 +565,16 @@ void TestCommandInteraction::TestDynamicEndpoint(nlTestSuite * apSuite, void * a
 
         app::ReadClient readClient(engine, &ctx.GetExchangeManager(), readCallback.mBufferedCallback,
                                    app::ReadClient::InteractionType::Subscribe);
+        // Enable the new endpoint
+        emberAfSetDynamicEndpoint(0, kTestEndpointId, &testEndpoint, Span<DataVersion>(dataVersionStorage));
 
         NL_TEST_ASSERT(apSuite, readClient.SendRequest(readParams) == CHIP_NO_ERROR);
 
         ctx.DrainAndServiceIO();
 
-        // We should not receive any reports in initial reports, so check mOnSubscriptionEstablished instead.
         NL_TEST_ASSERT(apSuite, readCallback.mOnSubscriptionEstablished);
         readCallback.mAttributeCount = 0;
 
-        // Enable the new endpoint
         emberAfSetDynamicEndpoint(0, kTestEndpointId4, &testEndpoint4, Span<DataVersion>(dataVersionStorage));
 
         ctx.DrainAndServiceIO();
@@ -623,7 +623,6 @@ void TestCommandInteraction::TestDynamicEndpoint(nlTestSuite * apSuite, void * a
 
     emberAfClearDynamicEndpoint(0);
 }
-
 /*
  * The tests below are for testing deatiled bwhavior when the attributes are modified between two chunks. In this test, we only care
  * above whether we will receive correct attribute values in reasonable messages with reduced reporting traffic.
@@ -737,7 +736,7 @@ void DoTest(TestMutableReadCallback * callback, Instruction instruction)
 
 }; // namespace TestSetDirtyBetweenChunksUtil
 
-void TestCommandInteraction::TestSetDirtyBetweenChunks(nlTestSuite * apSuite, void * apContext)
+void TestReadChunking::TestSetDirtyBetweenChunks(nlTestSuite * apSuite, void * apContext)
 {
     using namespace TestSetDirtyBetweenChunksUtil;
     TestContext & ctx                    = *static_cast<TestContext *>(apContext);
@@ -895,11 +894,11 @@ void TestCommandInteraction::TestSetDirtyBetweenChunks(nlTestSuite * apSuite, vo
 // clang-format off
 const nlTest sTests[] =
 {
-    NL_TEST_DEF("TestChunking", TestCommandInteraction::TestChunking),
-    NL_TEST_DEF("TestListChunking", TestCommandInteraction::TestListChunking),
-    NL_TEST_DEF("TestBadChunking", TestCommandInteraction::TestBadChunking),
-    NL_TEST_DEF("TestDynamicEndpoint", TestCommandInteraction::TestDynamicEndpoint),
-    NL_TEST_DEF("TestSetDirtyBetweenChunks", TestCommandInteraction::TestSetDirtyBetweenChunks),
+    NL_TEST_DEF("TestChunking", TestReadChunking::TestChunking),
+    NL_TEST_DEF("TestListChunking", TestReadChunking::TestListChunking),
+    NL_TEST_DEF("TestBadChunking", TestReadChunking::TestBadChunking),
+    NL_TEST_DEF("TestDynamicEndpoint", TestReadChunking::TestDynamicEndpoint),
+    NL_TEST_DEF("TestSetDirtyBetweenChunks", TestReadChunking::TestSetDirtyBetweenChunks),
     NL_TEST_SENTINEL()
 };
 
diff --git a/src/controller/tests/data_model/TestRead.cpp b/src/controller/tests/data_model/TestRead.cpp
index 15b2ee6c1f78bf..ff4359659f1f47 100644
--- a/src/controller/tests/data_model/TestRead.cpp
+++ b/src/controller/tests/data_model/TestRead.cpp
@@ -232,6 +232,12 @@ bool IsDeviceTypeOnEndpoint(DeviceTypeId deviceType, EndpointId endpoint)
 {
     return false;
 }
+
+bool ConcreteAttributePathExists(const ConcreteAttributePath & aPath)
+{
+    return true;
+}
+
 } // namespace app
 } // namespace chip
 
@@ -275,6 +281,7 @@ class TestReadInteraction : public app::ReadHandler::ApplicationCallback
     static void TestReadAttribute_ManyDataValues(nlTestSuite * apSuite, void * apContext);
     static void TestReadAttribute_ManyDataValuesWrongPath(nlTestSuite * apSuite, void * apContext);
     static void TestReadAttribute_ManyErrors(nlTestSuite * apSuite, void * apContext);
+    static void TestSubscribeAttributeDeniedNotExistPath(nlTestSuite * apSuite, void * apContext);
 
 private:
     static uint16_t mMaxInterval;
@@ -1550,9 +1557,9 @@ void TestReadInteraction::TestResubscribeAttributeTimeout(nlTestSuite * apSuite,
         app::AttributePathParams attributePathParams[1];
         readPrepareParams.mpAttributePathParamsList    = attributePathParams;
         readPrepareParams.mAttributePathParamsListSize = ArraySize(attributePathParams);
-
-        attributePathParams[0].mClusterId   = app::Clusters::TestCluster::Id;
-        attributePathParams[0].mAttributeId = app::Clusters::TestCluster::Attributes::Boolean::Id;
+        attributePathParams[0].mEndpointId             = kTestEndpointId;
+        attributePathParams[0].mClusterId              = app::Clusters::TestCluster::Id;
+        attributePathParams[0].mAttributeId            = app::Clusters::TestCluster::Attributes::Boolean::Id;
 
         readPrepareParams.mMaxIntervalCeilingSeconds = 1;
 
@@ -1626,6 +1633,7 @@ void TestReadInteraction::TestSubscribeAttributeTimeout(nlTestSuite * apSuite, v
         app::AttributePathParams attributePathParams[1];
         readPrepareParams.mpAttributePathParamsList    = attributePathParams;
         readPrepareParams.mAttributePathParamsListSize = ArraySize(attributePathParams);
+        attributePathParams[0].mEndpointId             = kTestEndpointId;
         attributePathParams[0].mClusterId              = app::Clusters::TestCluster::Id;
         attributePathParams[0].mAttributeId            = app::Clusters::TestCluster::Attributes::Boolean::Id;
 
@@ -2885,6 +2893,47 @@ void EstablishReadOrSubscriptions(nlTestSuite * apSuite, const SessionHandle & s
 
 } // namespace SubscriptionPathQuotaHelpers
 
+void TestReadInteraction::TestSubscribeAttributeDeniedNotExistPath(nlTestSuite * apSuite, void * apContext)
+{
+    TestContext & ctx  = *static_cast<TestContext *>(apContext);
+    auto sessionHandle = ctx.GetSessionBobToAlice();
+
+    ctx.SetMRPMode(Test::MessagingContext::MRPMode::kResponsive);
+
+    {
+        SubscriptionPathQuotaHelpers::TestReadCallback callback;
+        app::ReadClient readClient(app::InteractionModelEngine::GetInstance(), &ctx.GetExchangeManager(), callback,
+                                   app::ReadClient::InteractionType::Subscribe);
+
+        app::ReadPrepareParams readPrepareParams(ctx.GetSessionBobToAlice());
+
+        app::AttributePathParams attributePathParams[1];
+        readPrepareParams.mpAttributePathParamsList    = attributePathParams;
+        readPrepareParams.mAttributePathParamsListSize = ArraySize(attributePathParams);
+        attributePathParams[0].mClusterId              = app::Clusters::TestCluster::Id;
+        attributePathParams[0].mAttributeId            = app::Clusters::TestCluster::Attributes::ListStructOctetString::Id;
+
+        //
+        // Request a max interval that's very small to reduce time to discovering a liveness failure.
+        //
+        readPrepareParams.mMaxIntervalCeilingSeconds = 1;
+
+        auto err = readClient.SendRequest(readPrepareParams);
+        NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR);
+
+        ctx.DrainAndServiceIO();
+
+        NL_TEST_ASSERT(apSuite, callback.mOnError == 1);
+        NL_TEST_ASSERT(apSuite, callback.mLastError == CHIP_IM_GLOBAL_STATUS(InvalidAction));
+        NL_TEST_ASSERT(apSuite, callback.mOnDone == 1);
+    }
+
+    ctx.SetMRPMode(Test::MessagingContext::MRPMode::kDefault);
+
+    app::InteractionModelEngine::GetInstance()->ShutdownActiveReads();
+    NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
+}
+
 void TestReadInteraction::TestReadHandler_KillOverQuotaSubscriptions(nlTestSuite * apSuite, void * apContext)
 {
     // Note: We cannot use ctx.DrainAndServiceIO() since the perpetual read will make DrainAndServiceIO never return.
@@ -4436,6 +4485,7 @@ const nlTest sTests[] =
     NL_TEST_DEF("TestReadAttribute_ManyDataValues", TestReadInteraction::TestReadAttribute_ManyDataValues),
     NL_TEST_DEF("TestReadAttribute_ManyDataValuesWrongPath", TestReadInteraction::TestReadAttribute_ManyDataValuesWrongPath),
     NL_TEST_DEF("TestReadAttribute_ManyErrors", TestReadInteraction::TestReadAttribute_ManyErrors),
+    NL_TEST_DEF("TestSubscribeAttributeDeniedNotExistPath", TestReadInteraction::TestSubscribeAttributeDeniedNotExistPath),
     NL_TEST_DEF("TestResubscribeAttributeTimeout", TestReadInteraction::TestResubscribeAttributeTimeout),
     NL_TEST_DEF("TestSubscribeAttributeTimeout", TestReadInteraction::TestSubscribeAttributeTimeout),
     NL_TEST_SENTINEL()
diff --git a/src/darwin/Framework/CHIP/MTRIMDispatch.mm b/src/darwin/Framework/CHIP/MTRIMDispatch.mm
index c8b1df9f980a21..7979c34a832329 100644
--- a/src/darwin/Framework/CHIP/MTRIMDispatch.mm
+++ b/src/darwin/Framework/CHIP/MTRIMDispatch.mm
@@ -121,6 +121,11 @@ CHIP_ERROR ReadSingleClusterData(const SubjectDescriptor & aSubjectDescriptor, b
         return aAttributeReports.EncodeAttributeStatus(aPath, StatusIB(status));
     }
 
+    bool ConcreteAttributePathExists(const ConcreteAttributePath & aPath)
+    {
+        return DetermineAttributeStatus(aPath, /* aIsWrite = */ false) == Status::UnsupportedAccess;
+    }
+
     Status ServerClusterCommandExists(const ConcreteCommandPath & aPath)
     {
         // TODO: Consider making this configurable for applications that are not

From 8016e0fdf6bd977c5a1e3cd337797ccac206e49b Mon Sep 17 00:00:00 2001
From: Boris Zbarsky <bzbarsky@apple.com>
Date: Thu, 8 Sep 2022 12:35:10 -0400
Subject: [PATCH 2/2] Fix Darwin subscription test to follow the new failure
 semantics.

---
 src/darwin/Framework/CHIP/MTRBaseDevice.mm      |  8 +++++++-
 src/darwin/Framework/CHIPTests/MTRDeviceTests.m | 10 +++++++---
 2 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm
index 631291b9963fec..d5ffc41f529127 100644
--- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm
+++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm
@@ -1218,7 +1218,11 @@ - (void)subscribeAttributeWithEndpointId:(NSNumber * _Nullable)endpointId
             auto readClient = Platform::New<app::ReadClient>(
                 engine, exchangeManager, callback->GetBufferedCallback(), chip::app::ReadClient::InteractionType::Subscribe);
 
-            err = readClient->SendAutoResubscribeRequest(std::move(readParams));
+            if (params != nil && params.autoResubscribe != nil && ![params.autoResubscribe boolValue]) {
+                err = readClient->SendRequest(readParams);
+            } else {
+                err = readClient->SendAutoResubscribeRequest(std::move(readParams));
+            }
 
             if (err != CHIP_NO_ERROR) {
                 if (reportHandler) {
@@ -1227,6 +1231,8 @@ - (void)subscribeAttributeWithEndpointId:(NSNumber * _Nullable)endpointId
                     });
                 }
                 Platform::Delete(readClient);
+                Platform::Delete(container.pathParams);
+                container.pathParams = nullptr;
                 return;
             }
 
diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
index 1e95514ecc1e34..bb6f81a6e12be8 100644
--- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
@@ -643,10 +643,12 @@ - (void)test009_SubscribeFailure
     __block void (^reportHandler)(id _Nullable values, NSError * _Nullable error) = nil;
 
     // Set up expectation for report
-    XCTestExpectation * errorReportExpectation = [self expectationWithDescription:@"receive OnOff attribute report"];
+    XCTestExpectation * errorReportExpectation = [self expectationWithDescription:@"receive subscription error"];
     reportHandler = ^(id _Nullable value, NSError * _Nullable error) {
+        // Because our subscription has no existent paths, it gets an
+        // InvalidAction response, which is EMBER_ZCL_STATUS_MALFORMED_COMMAND.
         XCTAssertNil(value);
-        XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], EMBER_ZCL_STATUS_UNSUPPORTED_ENDPOINT);
+        XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], EMBER_ZCL_STATUS_MALFORMED_COMMAND);
         [errorReportExpectation fulfill];
     };
 
@@ -662,12 +664,14 @@ - (void)test009_SubscribeFailure
                                          }];
     [self waitForExpectations:@[ cleanSubscriptionExpectation ] timeout:kTimeoutInSeconds];
 
+    __auto_type * params = [[MTRSubscribeParams alloc] init];
+    params.autoResubscribe = @(NO);
     [device subscribeAttributeWithEndpointId:@10000
         clusterId:@6
         attributeId:@0
         minInterval:@2
         maxInterval:@10
-        params:nil
+        params:params
         clientQueue:queue
         reportHandler:^(id _Nullable values, NSError * _Nullable error) {
             NSLog(@"report attribute: OnOff values: %@, error: %@", values, error);