Skip to content

Commit 554f284

Browse files
authored
[ACE] perform initial ACL checks before existence checks to avoid leaking non-existence (project-chip#39415)
* CommandHandler: returning AccessFailures before Existence Failures * WriteHandler: returning ACL check failures before Attribute existence failures * Adapting TC_ACE_2.2 for WriteHandler Changes * Adapting changes to new spec * removing TODO comment * Adding Unit Tests * minor fixes and gemini comments * Unit Tests fix * Integrating Comments
1 parent b641cdb commit 554f284

File tree

10 files changed

+708
-38
lines changed

10 files changed

+708
-38
lines changed

src/app/CommandHandlerImpl.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,11 @@ Status CommandHandlerImpl::ProcessGroupCommandDataIB(CommandDataIB::Parser & aCo
536536
request.subjectDescriptor = &subjectDescriptor;
537537
request.invokeFlags.Set(DataModel::InvokeFlags::kTimed, IsTimedInvoke());
538538

539+
// SPEC-DIVERGENCE: The spec mandates only one ACL check after the existence check for non-concrete paths (Group
540+
// Commands). However, calling ValidateCommandCanBeDispatched here introduces an additional ACL check before the
541+
// existence check, because that function also performs an early access check (it is shared with the concrete path
542+
// case). This results in two ACL checks for group commands. In practice, this divergence is not observable if all
543+
// commands require at least Operate privilege.
539544
Status preCheckStatus = mpCallback->ValidateCommandCanBeDispatched(request);
540545
if (preCheckStatus != Status::Success)
541546
{

src/app/InteractionModelEngine.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,7 +1796,16 @@ Protocols::InteractionModel::Status InteractionModelEngine::ValidateCommandCanBe
17961796

17971797
DataModel::AcceptedCommandEntry acceptedCommandEntry;
17981798

1799-
Status status = CheckCommandExistence(request.path, acceptedCommandEntry);
1799+
// Execute the ACL Access Granting Algorithm before existence checks, assuming the required_privilege for the element is
1800+
// Operate, to determine if the subject would have had at least some access against the concrete path. This is done so we don't
1801+
// leak information if we do fail existence checks.
1802+
// SPEC-DIVERGENCE: For non-concrete paths (Group Commands), the spec mandates only one ACL check AFTER the existence check.
1803+
// However, because this code is also used in the group path case, we end up performing an ADDITIONAL ACL check before the
1804+
// existence check. In practice, this divergence is not observable if all commands require at least Operate privilege.
1805+
Status status = CheckCommandAccess(request, Access::Privilege::kOperate);
1806+
VerifyOrReturnValue(status == Status::Success, status);
1807+
1808+
status = CheckCommandExistence(request.path, acceptedCommandEntry);
18001809

18011810
if (status != Status::Success)
18021811
{
@@ -1805,14 +1814,14 @@ Protocols::InteractionModel::Status InteractionModelEngine::ValidateCommandCanBe
18051814
return status;
18061815
}
18071816

1808-
status = CheckCommandAccess(request, acceptedCommandEntry);
1817+
status = CheckCommandAccess(request, acceptedCommandEntry.GetInvokePrivilege());
18091818
VerifyOrReturnValue(status == Status::Success, status);
18101819

18111820
return CheckCommandFlags(request, acceptedCommandEntry);
18121821
}
18131822

18141823
Protocols::InteractionModel::Status InteractionModelEngine::CheckCommandAccess(const DataModel::InvokeRequest & aRequest,
1815-
const DataModel::AcceptedCommandEntry & entry)
1824+
const Access::Privilege aRequiredPrivilege)
18161825
{
18171826
if (aRequest.subjectDescriptor == nullptr)
18181827
{
@@ -1824,7 +1833,7 @@ Protocols::InteractionModel::Status InteractionModelEngine::CheckCommandAccess(c
18241833
.requestType = Access::RequestType::kCommandInvokeRequest,
18251834
.entityId = aRequest.path.mCommandId };
18261835

1827-
CHIP_ERROR err = Access::GetAccessControl().Check(*aRequest.subjectDescriptor, requestPath, entry.GetInvokePrivilege());
1836+
CHIP_ERROR err = Access::GetAccessControl().Check(*aRequest.subjectDescriptor, requestPath, aRequiredPrivilege);
18281837
if (err != CHIP_NO_ERROR)
18291838
{
18301839
if ((err != CHIP_ERROR_ACCESS_DENIED) && (err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL))

src/app/InteractionModelEngine.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
630630
* Validates that the command exists and on success returns the data for the command in `entry`.
631631
*/
632632
Status CheckCommandExistence(const ConcreteCommandPath & aCommandPath, DataModel::AcceptedCommandEntry & entry);
633-
Status CheckCommandAccess(const DataModel::InvokeRequest & aRequest, const DataModel::AcceptedCommandEntry & entry);
633+
Status CheckCommandAccess(const DataModel::InvokeRequest & aRequest, const Access::Privilege aRequiredPrivilege);
634634
Status CheckCommandFlags(const DataModel::InvokeRequest & aRequest, const DataModel::AcceptedCommandEntry & entry);
635635

636636
/**

src/app/WriteHandler.cpp

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -764,14 +764,15 @@ void WriteHandler::MoveToState(const State aTargetState)
764764
DataModel::ActionReturnStatus WriteHandler::CheckWriteAllowed(const Access::SubjectDescriptor & aSubject,
765765
const ConcreteAttributePath & aPath)
766766
{
767-
// TODO: ordering is to check writability/existence BEFORE ACL and this seems wrong, however
768-
// existing unit tests (TC_AcessChecker.py) validate that we get UnsupportedWrite instead of UnsupportedAccess
769-
//
770-
// This should likely be fixed in spec (probably already fixed by
771-
// https://github.com/CHIP-Specifications/connectedhomeip-spec/pull/9024)
772-
// and tests and implementation
773-
//
774-
// Open issue that needs fixing: https://github.com/project-chip/connectedhomeip/issues/33735
767+
768+
// Execute the ACL Access Granting Algorithm before existence checks, assuming the required_privilege for the element is
769+
// View, to determine if the subject would have had at least some access against the concrete path. This is done so we don't
770+
// leak information if we do fail existence checks.
771+
// SPEC-DIVERGENCE: For non-concrete paths, the spec mandates only one ACL check AFTER the existence check.
772+
// However, because this code is also used in the group path case, we end up performing an ADDITIONAL ACL check before the
773+
// existence check. In practice, this divergence is not observable.
774+
Status writeAccessStatus = CheckWriteAccess(aSubject, aPath, Access::Privilege::kView);
775+
VerifyOrReturnValue(writeAccessStatus == Status::Success, writeAccessStatus);
775776

776777
DataModel::AttributeFinder finder(mDataModelProvider);
777778

@@ -790,6 +791,21 @@ DataModel::ActionReturnStatus WriteHandler::CheckWriteAllowed(const Access::Subj
790791
// Allow writes on writable attributes only
791792
VerifyOrReturnValue(attributeEntry->GetWritePrivilege().has_value(), Status::UnsupportedWrite);
792793

794+
// Execute the ACL Access Granting Algorithm against the concrete path a second time, using the actual required_privilege
795+
writeAccessStatus = CheckWriteAccess(aSubject, aPath, *attributeEntry->GetWritePrivilege());
796+
VerifyOrReturnValue(writeAccessStatus == Status::Success, writeAccessStatus);
797+
798+
// validate that timed write is enforced
799+
VerifyOrReturnValue(IsTimedWrite() || !attributeEntry->HasFlags(DataModel::AttributeQualityFlags::kTimed),
800+
Status::NeedsTimedInteraction);
801+
802+
return Status::Success;
803+
}
804+
805+
Status WriteHandler::CheckWriteAccess(const Access::SubjectDescriptor & aSubject, const ConcreteAttributePath & aPath,
806+
const Access::Privilege aRequiredPrivilege)
807+
{
808+
793809
bool checkAcl = true;
794810
if (mLastSuccessfullyWrittenPath.has_value())
795811
{
@@ -812,22 +828,25 @@ DataModel::ActionReturnStatus WriteHandler::CheckWriteAllowed(const Access::Subj
812828
.requestType = Access::RequestType::kAttributeWriteRequest,
813829
.entityId = aPath.mAttributeId };
814830

815-
// NOTE: we know that attributeEntry has a GetWriteProvilege based on the check above.
816-
// so we just directly reference it.
817-
CHIP_ERROR err = Access::GetAccessControl().Check(aSubject, requestPath, *attributeEntry->GetWritePrivilege());
831+
CHIP_ERROR err = Access::GetAccessControl().Check(aSubject, requestPath, aRequiredPrivilege);
818832

819-
if (err != CHIP_NO_ERROR)
833+
if (err == CHIP_NO_ERROR)
820834
{
821-
VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_DENIED, Status::UnsupportedAccess);
822-
VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL, Status::AccessRestricted);
835+
return Status::Success;
836+
}
823837

824-
return err;
838+
if (err == CHIP_ERROR_ACCESS_DENIED)
839+
{
840+
return Status::UnsupportedAccess;
825841
}
826-
}
827842

828-
// validate that timed write is enforced
829-
VerifyOrReturnValue(IsTimedWrite() || !attributeEntry->HasFlags(DataModel::AttributeQualityFlags::kTimed),
830-
Status::NeedsTimedInteraction);
843+
if (err == CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL)
844+
{
845+
return Status::AccessRestricted;
846+
}
847+
848+
return Status::Failure;
849+
}
831850

832851
return Status::Success;
833852
}

src/app/WriteHandler.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@ class WriteHandler : public Messaging::ExchangeDelegate
194194
DataModel::ActionReturnStatus CheckWriteAllowed(const Access::SubjectDescriptor & aSubject,
195195
const ConcreteAttributePath & aPath);
196196

197+
/// Validate that a write on the given path has aRequiredPrivilege.
198+
///
199+
/// Returns a success status if the ACL check is successful, otherwise the relevant status will be returned.
200+
Status CheckWriteAccess(const Access::SubjectDescriptor & aSubject, const ConcreteAttributePath & aPath,
201+
const Access::Privilege aRequiredPrivilege);
202+
197203
// Write the given data to the given path
198204
CHIP_ERROR WriteClusterData(const Access::SubjectDescriptor & aSubject, const ConcreteDataAttributePath & aPath,
199205
TLV::TLVReader & aData);

src/app/tests/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ chip_test_suite("tests") {
249249

250250
test_sources = [
251251
"TestAclAttribute.cpp",
252+
"TestAclCommand.cpp",
252253
"TestAclEvent.cpp",
253254
"TestActionsCluster.cpp",
254255
"TestAttributeAccessInterfaceCache.cpp",

0 commit comments

Comments
 (0)