From f87af853e2db99269acea572dcc109bc6f797aa9 Mon Sep 17 00:00:00 2001
From: Joseph Sutton <josephsutton@catalyst.net.nz>
Date: Fri, 3 Mar 2023 17:34:29 +1300
Subject: [PATCH 24/34] CVE-2023-0614 ldb: Prevent disclosure of confidential
 attributes

Add a hook, acl_redact_msg_for_filter(), in the aclread module, that
marks inaccessible any message elements used by an LDAP search filter
that the user has no right to access. Make the various ldb_match_*()
functions check whether message elements are accessible, and refuse to
match any that are not. Remaining message elements, not mentioned in the
search filter, are checked in aclread_callback(), and any inaccessible
elements are removed at this point.

Certain attributes, namely objectClass, distinguishedName, name, and
objectGUID, are always present, and hence the presence of said
attributes is always allowed to be checked in a search filter. This
corresponds with the behaviour of Windows.

Further, we unconditionally allow the attributes isDeleted and
isRecycled in a check for presence or equality. Windows is not known to
make this special exception, but it seems mostly harmless, and should
mitigate the performance impact on searches made by the show_deleted
module.

As a result of all these changes, our behaviour regarding confidential
attributes happens to match Windows more closely. For the test in
confidential_attr.py, we can now model our attribute handling with
DC_MODE_RETURN_ALL, which corresponds to the behaviour exhibited by
Windows.

BUG: https://bugzilla.samba.org/show_bug.cgi?id=15270

Pair-Programmed-With: Andrew Bartlett <abartlet@samba.org>

Signed-off-by: Joseph Sutton <josephsutton@catalyst.net.nz>
Signed-off-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>

[abartlet@samba.org adapted due to Samba 4.17 and lower
not having the patches for CVE-2020-25720]

Conflict: NA
Reference: https://attachments.samba.org/attachment.cgi?id=17821
---
 lib/ldb-samba/ldb_matching_rules.c            |  15 +
 lib/ldb/common/ldb_match.c                    |  37 +
 lib/ldb/include/ldb_module.h                  |  11 +
 lib/ldb/include/ldb_private.h                 |   5 +
 lib/ldb/ldb_key_value/ldb_kv_index.c          |   8 +
 lib/ldb/ldb_key_value/ldb_kv_search.c         |  15 +
 selftest/knownfail.d/confidential-attr-timing |   1 -
 source4/dsdb/samdb/ldb_modules/acl.c          | 183 +---
 source4/dsdb/samdb/ldb_modules/acl_read.c     | 837 ++++++++++++------
 source4/dsdb/samdb/samdb.h                    |   2 +
 .../dsdb/tests/python/confidential_attr.py    |  12 +-
 source4/setup/schema_samba4.ldif              |   1 +
 12 files changed, 672 insertions(+), 455 deletions(-)
 delete mode 100644 selftest/knownfail.d/confidential-attr-timing

diff --git a/lib/ldb-samba/ldb_matching_rules.c b/lib/ldb-samba/ldb_matching_rules.c
index 827f3920ae8..0c4c31e49f9 100644
--- a/lib/ldb-samba/ldb_matching_rules.c
+++ b/lib/ldb-samba/ldb_matching_rules.c
@@ -87,6 +87,11 @@ static int ldb_eval_transitive_filter_helper(TALLOC_CTX *mem_ctx,
 		return LDB_SUCCESS;
 	}
 
+	if (ldb_msg_element_is_inaccessible(el)) {
+		*matched = false;
+		return LDB_SUCCESS;
+	}
+
 	/*
 	 * If the value to match is present in the attribute values of the
 	 * current entry being visited, set matched to true and return OK
@@ -370,6 +375,11 @@ static int dsdb_match_for_dns_to_tombstone_time(struct ldb_context *ldb,
 		return LDB_SUCCESS;
 	}
 
+	if (ldb_msg_element_is_inaccessible(el)) {
+		*matched = false;
+		return LDB_SUCCESS;
+	}
+
 	session_info = talloc_get_type(ldb_get_opaque(ldb, "sessionInfo"),
 				       struct auth_session_info);
 	if (session_info == NULL) {
@@ -489,6 +499,11 @@ static int dsdb_match_for_expunge(struct ldb_context *ldb,
 		return LDB_SUCCESS;
 	}
 
+	if (ldb_msg_element_is_inaccessible(el)) {
+		*matched = false;
+		return LDB_SUCCESS;
+	}
+
 	session_info
 		= talloc_get_type(ldb_get_opaque(ldb, DSDB_SESSION_INFO),
 				  struct auth_session_info);
diff --git a/lib/ldb/common/ldb_match.c b/lib/ldb/common/ldb_match.c
index 2f4d41f3441..17314f1b9ca 100644
--- a/lib/ldb/common/ldb_match.c
+++ b/lib/ldb/common/ldb_match.c
@@ -98,6 +98,11 @@ static int ldb_match_present(struct ldb_context *ldb,
 		return LDB_SUCCESS;
 	}
 
+	if (ldb_msg_element_is_inaccessible(el)) {
+		*matched = false;
+		return LDB_SUCCESS;
+	}
+
 	a = ldb_schema_attribute_by_name(ldb, el->name);
 	if (!a) {
 		return LDB_ERR_INVALID_ATTRIBUTE_SYNTAX;
@@ -139,6 +144,11 @@ static int ldb_match_comparison(struct ldb_context *ldb,
 		return LDB_SUCCESS;
 	}
 
+	if (ldb_msg_element_is_inaccessible(el)) {
+		*matched = false;
+		return LDB_SUCCESS;
+	}
+
 	a = ldb_schema_attribute_by_name(ldb, el->name);
 	if (!a) {
 		return LDB_ERR_INVALID_ATTRIBUTE_SYNTAX;
@@ -209,6 +219,11 @@ static int ldb_match_equality(struct ldb_context *ldb,
 		return LDB_SUCCESS;
 	}
 
+	if (ldb_msg_element_is_inaccessible(el)) {
+		*matched = false;
+		return LDB_SUCCESS;
+	}
+
 	a = ldb_schema_attribute_by_name(ldb, el->name);
 	if (a == NULL) {
 		return LDB_ERR_INVALID_ATTRIBUTE_SYNTAX;
@@ -370,6 +385,11 @@ static int ldb_match_substring(struct ldb_context *ldb,
 		return LDB_SUCCESS;
 	}
 
+	if (ldb_msg_element_is_inaccessible(el)) {
+		*matched = false;
+		return LDB_SUCCESS;
+	}
+
 	for (i = 0; i < el->num_values; i++) {
 		int ret;
 		ret = ldb_wildcard_compare(ldb, tree, el->values[i], matched);
@@ -443,6 +463,11 @@ static int ldb_match_bitmask(struct ldb_context *ldb,
 		return LDB_SUCCESS;
 	}
 
+	if (ldb_msg_element_is_inaccessible(el)) {
+		*matched = false;
+		return LDB_SUCCESS;
+	}
+
 	for (i=0;i<el->num_values;i++) {
 		int ret;
 		struct ldb_val *v = &el->values[i];
@@ -741,3 +766,15 @@ int ldb_register_extended_match_rule(struct ldb_context *ldb,
 	return LDB_SUCCESS;
 }
 
+int ldb_register_redact_callback(struct ldb_context *ldb,
+				 ldb_redact_fn redact_fn,
+				 struct ldb_module *module)
+{
+	if (ldb->redact.callback != NULL) {
+		return LDB_ERR_ENTRY_ALREADY_EXISTS;
+	}
+
+	ldb->redact.callback = redact_fn;
+	ldb->redact.module = module;
+	return LDB_SUCCESS;
+}
diff --git a/lib/ldb/include/ldb_module.h b/lib/ldb/include/ldb_module.h
index bd369ed9512..0f14b1ad346 100644
--- a/lib/ldb/include/ldb_module.h
+++ b/lib/ldb/include/ldb_module.h
@@ -102,6 +102,12 @@ struct ldb_module;
  */
 #define LDB_FLAG_INTERNAL_SHARED_VALUES 0x200
 
+/*
+ * this attribute has been access checked. We know the user has the right to
+ * view it. Used internally in Samba aclread module.
+ */
+#define LDB_FLAG_INTERNAL_ACCESS_CHECKED 0x400
+
 /* an extended match rule that always fails to match */
 #define SAMBA_LDAP_MATCH_ALWAYS_FALSE "1.3.6.1.4.1.7165.4.5.1"
 
@@ -520,6 +526,11 @@ void ldb_msg_element_mark_inaccessible(struct ldb_message_element *el);
 bool ldb_msg_element_is_inaccessible(const struct ldb_message_element *el);
 void ldb_msg_remove_inaccessible(struct ldb_message *msg);
 
+typedef int (*ldb_redact_fn)(struct ldb_module *, struct ldb_request *, struct ldb_message *);
+int ldb_register_redact_callback(struct ldb_context *ldb,
+			       ldb_redact_fn redact_fn,
+			       struct ldb_module *module);
+
 /*
  * these pack/unpack functions are exposed in the library for use by
  * ldb tools like ldbdump and for use in tests,
diff --git a/lib/ldb/include/ldb_private.h b/lib/ldb/include/ldb_private.h
index ca43817d07a..b0a42f6421c 100644
--- a/lib/ldb/include/ldb_private.h
+++ b/lib/ldb/include/ldb_private.h
@@ -119,6 +119,11 @@ struct ldb_context {
 		struct ldb_extended_match_entry *prev, *next;
 	} *extended_match_rules;
 
+	struct {
+		struct ldb_module *module;
+		ldb_redact_fn callback;
+	} redact;
+
 	/* custom utf8 functions */
 	struct ldb_utf8_fns utf8_fns;
 
diff --git a/lib/ldb/ldb_key_value/ldb_kv_index.c b/lib/ldb/ldb_key_value/ldb_kv_index.c
index 203266ea8c9..163052fecf7 100644
--- a/lib/ldb/ldb_key_value/ldb_kv_index.c
+++ b/lib/ldb/ldb_key_value/ldb_kv_index.c
@@ -2428,6 +2428,14 @@ static int ldb_kv_index_filter(struct ldb_kv_private *ldb_kv,
 			return LDB_ERR_OPERATIONS_ERROR;
 		}
 
+		if (ldb->redact.callback != NULL) {
+			ret = ldb->redact.callback(ldb->redact.module, ac->req, msg);
+			if (ret != LDB_SUCCESS) {
+				talloc_free(msg);
+				return ret;
+			}
+		}
+
 		/*
 		 * We trust the index for LDB_SCOPE_ONELEVEL
 		 * unless the index key has been truncated.
diff --git a/lib/ldb/ldb_key_value/ldb_kv_search.c b/lib/ldb/ldb_key_value/ldb_kv_search.c
index f3333510eab..d187ba223e1 100644
--- a/lib/ldb/ldb_key_value/ldb_kv_search.c
+++ b/lib/ldb/ldb_key_value/ldb_kv_search.c
@@ -395,6 +395,14 @@ static int search_func(_UNUSED_ struct ldb_kv_private *ldb_kv,
 		}
 	}
 
+	if (ldb->redact.callback != NULL) {
+		ret = ldb->redact.callback(ldb->redact.module, ac->req, msg);
+		if (ret != LDB_SUCCESS) {
+			talloc_free(msg);
+			return ret;
+		}
+	}
+
 	/* see if it matches the given expression */
 	ret = ldb_match_msg_error(ldb, msg,
 				  ac->tree, ac->base, ac->scope, &matched);
@@ -530,6 +538,13 @@ static int ldb_kv_search_and_return_base(struct ldb_kv_private *ldb_kv,
 		return ret;
 	}
 
+	if (ldb->redact.callback != NULL) {
+		ret = ldb->redact.callback(ldb->redact.module, ctx->req, msg);
+		if (ret != LDB_SUCCESS) {
+			talloc_free(msg);
+			return ret;
+		}
+	}
 
 	/*
 	 * We use this, not ldb_match_msg_error() as we know
diff --git a/selftest/knownfail.d/confidential-attr-timing b/selftest/knownfail.d/confidential-attr-timing
deleted file mode 100644
index e213cdb16d3..00000000000
--- a/selftest/knownfail.d/confidential-attr-timing
+++ /dev/null
@@ -1 +0,0 @@
-^samba4.ldap.confidential_attr.python\(ad_dc_slowtests\).__main__.ConfidentialAttrTestDirsync.test_timing_attack\(ad_dc_slowtests\)
diff --git a/source4/dsdb/samdb/ldb_modules/acl.c b/source4/dsdb/samdb/ldb_modules/acl.c
index 4098ae2d671..5c57dd25faa 100644
--- a/source4/dsdb/samdb/ldb_modules/acl.c
+++ b/source4/dsdb/samdb/ldb_modules/acl.c
@@ -46,11 +46,6 @@
 #undef strcasecmp
 #undef strncasecmp
 
-struct extended_access_check_attribute {
-	const char *oa_name;
-	const uint32_t requires_rights;
-};
-
 struct acl_private {
 	bool acl_search;
 	const char **password_attrs;
@@ -58,7 +53,6 @@ struct acl_private {
 	uint64_t cached_schema_metadata_usn;
 	uint64_t cached_schema_loaded_usn;
 	const char **confidential_attrs;
-	bool userPassword_support;
 };
 
 struct acl_context {
@@ -66,15 +60,12 @@ struct acl_context {
 	struct ldb_request *req;
 	bool am_system;
 	bool am_administrator;
-	bool modify_search;
 	bool constructed_attrs;
 	bool allowedAttributes;
 	bool allowedAttributesEffective;
 	bool allowedChildClasses;
 	bool allowedChildClassesEffective;
 	bool sDRightsEffective;
-	bool userPassword;
-	const char * const *attrs;
 	struct dsdb_schema *schema;
 };
 
@@ -83,25 +74,9 @@ static int acl_module_init(struct ldb_module *module)
 	struct ldb_context *ldb;
 	struct acl_private *data;
 	int ret;
-	unsigned int i, n, j;
-	TALLOC_CTX *mem_ctx;
-	static const char * const attrs[] = { "passwordAttribute", NULL };
-	static const char * const secret_attrs[] = {
-		DSDB_SECRET_ATTRIBUTES
-	};
-	struct ldb_result *res;
-	struct ldb_message *msg;
-	struct ldb_message_element *password_attributes;
 
 	ldb = ldb_module_get_ctx(module);
 
-	ret = ldb_mod_register_control(module, LDB_CONTROL_SD_FLAGS_OID);
-	if (ret != LDB_SUCCESS) {
-		ldb_debug(ldb, LDB_DEBUG_ERROR,
-			  "acl_module_init: Unable to register control with rootdse!\n");
-		return ldb_operr(ldb);
-	}
-
 	data = talloc_zero(module, struct acl_private);
 	if (data == NULL) {
 		return ldb_oom(ldb);
@@ -111,91 +86,14 @@ static int acl_module_init(struct ldb_module *module)
 					NULL, "acl", "search", true);
 	ldb_module_set_private(module, data);
 
-	mem_ctx = talloc_new(module);
-	if (!mem_ctx) {
-		return ldb_oom(ldb);
-	}
-
-	ret = dsdb_module_search_dn(module, mem_ctx, &res,
-				    ldb_dn_new(mem_ctx, ldb, "@KLUDGEACL"),
-				    attrs,
-				    DSDB_FLAG_NEXT_MODULE |
-				    DSDB_FLAG_AS_SYSTEM,
-				    NULL);
-	if (ret != LDB_SUCCESS) {
-		goto done;
-	}
-	if (res->count == 0) {
-		goto done;
-	}
-
-	if (res->count > 1) {
-		talloc_free(mem_ctx);
-		return LDB_ERR_CONSTRAINT_VIOLATION;
-	}
-
-	msg = res->msgs[0];
-
-	password_attributes = ldb_msg_find_element(msg, "passwordAttribute");
-	if (!password_attributes) {
-		goto done;
-	}
-	data->password_attrs = talloc_array(data, const char *,
-			password_attributes->num_values +
-			ARRAY_SIZE(secret_attrs) + 1);
-	if (!data->password_attrs) {
-		talloc_free(mem_ctx);
-		return ldb_oom(ldb);
-	}
-
-	n = 0;
-	for (i=0; i < password_attributes->num_values; i++) {
-		data->password_attrs[n] = (const char *)password_attributes->values[i].data;
-		talloc_steal(data->password_attrs, password_attributes->values[i].data);
-		n++;
-	}
-
-	for (i=0; i < ARRAY_SIZE(secret_attrs); i++) {
-		bool found = false;
-
-		for (j=0; j < n; j++) {
-			if (strcasecmp(data->password_attrs[j], secret_attrs[i]) == 0) {
-				found = true;
-				break;
-			}
-		}
-
-		if (found) {
-			continue;
-		}
-
-		data->password_attrs[n] = talloc_strdup(data->password_attrs,
-							secret_attrs[i]);
-		if (data->password_attrs[n] == NULL) {
-			talloc_free(mem_ctx);
-			return ldb_oom(ldb);
-		}
-		n++;
-	}
-	data->password_attrs[n] = NULL;
-
-done:
-	talloc_free(mem_ctx);
-	ret = ldb_next_init(module);
-
+	ret = ldb_mod_register_control(module, LDB_CONTROL_SD_FLAGS_OID);
 	if (ret != LDB_SUCCESS) {
-		return ret;
+		ldb_debug(ldb, LDB_DEBUG_ERROR,
+			  "acl_module_init: Unable to register control with rootdse!\n");
+		return ldb_operr(ldb);
 	}
 
-	/*
-	 * Check this after the modules have be initialised so we
-	 * can actually read the backend DB.
-	 */
-	data->userPassword_support
-		= dsdb_user_password_support(module,
-					     module,
-					     NULL);
-	return ret;
+	return ldb_next_init(module);
 }
 
 static int acl_allowedAttributes(struct ldb_module *module,
@@ -2522,29 +2420,11 @@ static int acl_search_callback(struct ldb_request *req, struct ldb_reply *ares)
 						     ares->controls);
 		}
 
-		if (data->password_attrs != NULL) {
-			for (i = 0; data->password_attrs[i]; i++) {
-				if ((!ac->userPassword) &&
-				    (ldb_attr_cmp(data->password_attrs[i],
-						  "userPassword") == 0))
-				{
-						continue;
-				}
-
-				ldb_msg_remove_attr(ares->message, data->password_attrs[i]);
-			}
-		}
-
 		if (ac->am_administrator) {
 			return ldb_module_send_entry(ac->req, ares->message,
 						     ares->controls);
 		}
 
-		ret = acl_search_update_confidential_attrs(ac, data);
-		if (ret != LDB_SUCCESS) {
-			return ret;
-		}
-
 		if (data->confidential_attrs != NULL) {
 			for (i = 0; data->confidential_attrs[i]; i++) {
 				ldb_msg_remove_attr(ares->message,
@@ -2569,11 +2449,12 @@ static int acl_search(struct ldb_module *module, struct ldb_request *req)
 {
 	struct ldb_context *ldb;
 	struct acl_context *ac;
-	struct ldb_parse_tree *down_tree;
+	struct ldb_parse_tree *down_tree = req->op.search.tree;
 	struct ldb_request *down_req;
 	struct acl_private *data;
 	int ret;
 	unsigned int i;
+	bool modify_search = true;
 
 	if (ldb_dn_is_special(req->op.search.base)) {
 		return ldb_next_request(module, req);
@@ -2592,13 +2473,11 @@ static int acl_search(struct ldb_module *module, struct ldb_request *req)
 	ac->am_system = dsdb_module_am_system(module);
 	ac->am_administrator = dsdb_module_am_administrator(module);
 	ac->constructed_attrs = false;
-	ac->modify_search = true;
 	ac->allowedAttributes = ldb_attr_in_list(req->op.search.attrs, "allowedAttributes");
 	ac->allowedAttributesEffective = ldb_attr_in_list(req->op.search.attrs, "allowedAttributesEffective");
 	ac->allowedChildClasses = ldb_attr_in_list(req->op.search.attrs, "allowedChildClasses");
 	ac->allowedChildClassesEffective = ldb_attr_in_list(req->op.search.attrs, "allowedChildClassesEffective");
 	ac->sDRightsEffective = ldb_attr_in_list(req->op.search.attrs, "sDRightsEffective");
-	ac->userPassword = true;
 	ac->schema = dsdb_get_schema(ldb, ac);
 
 	ac->constructed_attrs |= ac->allowedAttributes;
@@ -2608,13 +2487,13 @@ static int acl_search(struct ldb_module *module, struct ldb_request *req)
 	ac->constructed_attrs |= ac->sDRightsEffective;
 
 	if (data == NULL) {
-		ac->modify_search = false;
+		modify_search = false;
 	}
 	if (ac->am_system) {
-		ac->modify_search = false;
+		modify_search = false;
 	}
 
-	if (!ac->constructed_attrs && !ac->modify_search) {
+	if (!ac->constructed_attrs && !modify_search) {
 		talloc_free(ac);
 		return ldb_next_request(module, req);
 	}
@@ -2624,38 +2503,24 @@ static int acl_search(struct ldb_module *module, struct ldb_request *req)
 		return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR,
 				 "acl_private data is missing");
 	}
-	ac->userPassword = data->userPassword_support;
-
-	ret = acl_search_update_confidential_attrs(ac, data);
-	if (ret != LDB_SUCCESS) {
-		return ret;
-	}
 
-	down_tree = ldb_parse_tree_copy_shallow(ac, req->op.search.tree);
-	if (down_tree == NULL) {
-		return ldb_oom(ldb);
-	}
+	if (!ac->am_system && !ac->am_administrator) {
+		ret = acl_search_update_confidential_attrs(ac, data);
+		if (ret != LDB_SUCCESS) {
+			return ret;
+		}
 
-	if (!ac->am_system && data->password_attrs) {
-		for (i = 0; data->password_attrs[i]; i++) {
-			if ((!ac->userPassword) &&
-			    (ldb_attr_cmp(data->password_attrs[i],
-					  "userPassword") == 0))
-			{
-				continue;
+		if (data->confidential_attrs != NULL) {
+			down_tree = ldb_parse_tree_copy_shallow(ac, req->op.search.tree);
+			if (down_tree == NULL) {
+				return ldb_oom(ldb);
 			}
 
-			ldb_parse_tree_attr_replace(down_tree,
-						    data->password_attrs[i],
-						    "kludgeACLredactedattribute");
-		}
-	}
-
-	if (!ac->am_system && !ac->am_administrator && data->confidential_attrs) {
-		for (i = 0; data->confidential_attrs[i]; i++) {
-			ldb_parse_tree_attr_replace(down_tree,
-						    data->confidential_attrs[i],
-						    "kludgeACLredactedattribute");
+			for (i = 0; data->confidential_attrs[i]; i++) {
+				ldb_parse_tree_attr_replace(down_tree,
+							    data->confidential_attrs[i],
+							    "kludgeACLredactedattribute");
+			}
 		}
 	}
 
diff --git a/source4/dsdb/samdb/ldb_modules/acl_read.c b/source4/dsdb/samdb/ldb_modules/acl_read.c
index 6659c71c965..8ca8607b925 100644
--- a/source4/dsdb/samdb/ldb_modules/acl_read.c
+++ b/source4/dsdb/samdb/ldb_modules/acl_read.c
@@ -37,20 +37,25 @@
 #include "librpc/gen_ndr/ndr_security.h"
 #include "param/param.h"
 #include "dsdb/samdb/ldb_modules/util.h"
+#include "lib/util/binsearch.h"
 
 #undef strcasecmp
 
+struct ldb_attr_vec {
+	const char** attrs;
+	size_t len;
+	size_t capacity;
+};
+
 struct aclread_context {
 	struct ldb_module *module;
 	struct ldb_request *req;
-	const char * const *attrs;
 	const struct dsdb_schema *schema;
 	uint32_t sd_flags;
 	bool added_nTSecurityDescriptor;
 	bool added_instanceType;
 	bool added_objectSid;
 	bool added_objectClass;
-	bool indirsync;
 
 	bool do_list_object_initialized;
 	bool do_list_object;
@@ -60,6 +65,11 @@ struct aclread_context {
 	/* cache on the last parent we checked in this search */
 	struct ldb_dn *last_parent_dn;
 	int last_parent_check_ret;
+
+	bool am_administrator;
+
+	bool got_tree_attrs;
+	struct ldb_attr_vec tree_attrs;
 };
 
 struct aclread_private {
@@ -68,6 +78,7 @@ struct aclread_private {
 	/* cache of the last SD we read during any search */
 	struct security_descriptor *sd_cached;
 	struct ldb_val sd_cached_blob;
+	const char **password_attrs;
 };
 
 struct access_check_context {
@@ -77,6 +88,183 @@ struct access_check_context {
 	const struct dsdb_class *objectclass;
 };
 
+static void acl_element_mark_access_checked(struct ldb_message_element *el)
+{
+	el->flags |= LDB_FLAG_INTERNAL_ACCESS_CHECKED;
+}
+
+static bool acl_element_is_access_checked(const struct ldb_message_element *el)
+{
+	return (el->flags & LDB_FLAG_INTERNAL_ACCESS_CHECKED) != 0;
+}
+
+static bool attr_in_vec(const struct ldb_attr_vec *vec, const char *attr)
+{
+	const char **found = NULL;
+
+	if (vec == NULL) {
+		return false;
+	}
+
+	BINARY_ARRAY_SEARCH_V(vec->attrs,
+			      vec->len,
+			      attr,
+			      ldb_attr_cmp,
+			      found);
+	return found != NULL;
+}
+
+static int acl_attr_cmp_fn(const char *a, const char **b)
+{
+	return ldb_attr_cmp(a, *b);
+}
+
+static int attr_vec_add_unique(TALLOC_CTX *mem_ctx,
+			       struct ldb_attr_vec *vec,
+			       const char *attr)
+{
+	const char **exact = NULL;
+	const char **next = NULL;
+	size_t next_idx = 0;
+
+	BINARY_ARRAY_SEARCH_GTE(vec->attrs,
+				vec->len,
+				attr,
+				acl_attr_cmp_fn,
+				exact,
+				next);
+	if (exact != NULL) {
+		return LDB_SUCCESS;
+	}
+
+	if (vec->len == SIZE_MAX) {
+		return LDB_ERR_OPERATIONS_ERROR;
+	}
+
+	if (next != NULL) {
+		next_idx = next - vec->attrs;
+	}
+
+	if (vec->len >= vec->capacity) {
+		const char **attrs = NULL;
+
+		if (vec->capacity == 0) {
+			vec->capacity = 4;
+		} else {
+			if (vec->capacity > SIZE_MAX / 2) {
+				return LDB_ERR_OPERATIONS_ERROR;
+			}
+			vec->capacity *= 2;
+		}
+
+		attrs = talloc_realloc(mem_ctx, vec->attrs, const char *, vec->capacity);
+		if (attrs == NULL) {
+			return LDB_ERR_OPERATIONS_ERROR;
+		}
+
+		vec->attrs = attrs;
+	}
+	SMB_ASSERT(vec->len < vec->capacity);
+
+	if (next == NULL) {
+		vec->attrs[vec->len++] = attr;
+	} else {
+		size_t count = (vec->len - next_idx) * sizeof (vec->attrs[0]);
+		memmove(&vec->attrs[next_idx + 1],
+			&vec->attrs[next_idx],
+			count);
+
+		vec->attrs[next_idx] = attr;
+		++vec->len;
+	}
+
+	return LDB_SUCCESS;
+}
+
+static bool ldb_attr_always_present(const char *attr)
+{
+	static const char * const attrs_always_present[] = {
+		"objectClass",
+		"distinguishedName",
+		"name",
+		"objectGUID",
+		NULL
+	};
+
+	return ldb_attr_in_list(attrs_always_present, attr);
+}
+
+static bool ldb_attr_always_visible(const char *attr)
+{
+	static const char * const attrs_always_visible[] = {
+		"isDeleted",
+		"isRecycled",
+		NULL
+	};
+
+	return ldb_attr_in_list(attrs_always_visible, attr);
+}
+
+/* Collect a list of attributes required to match a given parse tree. */
+static int ldb_parse_tree_collect_acl_attrs(struct ldb_module *module,
+					    TALLOC_CTX *mem_ctx,
+					    struct ldb_attr_vec *attrs,
+					    const struct ldb_parse_tree *tree)
+{
+	const char *attr = NULL;
+	unsigned int i;
+	int ret;
+
+	if (tree == NULL) {
+		return 0;
+	}
+
+	switch (tree->operation) {
+	case LDB_OP_OR:
+	case LDB_OP_AND:		/* attributes stored in list of subtrees */
+		for (i = 0; i < tree->u.list.num_elements; i++) {
+			ret = ldb_parse_tree_collect_acl_attrs(module, mem_ctx,
+							       attrs, tree->u.list.elements[i]);
+			if (ret) {
+				return ret;
+			}
+		}
+		return 0;
+
+	case LDB_OP_NOT:		/* attributes stored in single subtree */
+		return ldb_parse_tree_collect_acl_attrs(module, mem_ctx, attrs, tree->u.isnot.child);
+
+	case LDB_OP_PRESENT:
+		/*
+		 * If the search filter is checking for an attribute's presence,
+		 * and the attribute is always present, we can skip access
+		 * rights checks. Every object has these attributes, and so
+		 * there's no security reason to hide their presence.
+		 * Note: the acl.py tests (e.g. test_search1()) rely on this
+		 * exception.  I.e. even if we lack Read Property (RP) rights
+		 * for a child object, it should still appear as a visible
+		 * object in 'objectClass=*' searches, so long as we have List
+		 * Contents (LC) rights for the object.
+		 */
+		if (ldb_attr_always_present(tree->u.present.attr)) {
+			/* No need to check this attribute. */
+			return 0;
+		}
+
+		FALL_THROUGH;
+	case LDB_OP_EQUALITY:
+		if (ldb_attr_always_visible(tree->u.present.attr)) {
+			/* No need to check this attribute. */
+			return 0;
+		}
+
+		FALL_THROUGH;
+	default:			/* single attribute in tree */
+		attr = ldb_parse_tree_get_attr(tree);
+		return attr_vec_add_unique(mem_ctx, attrs, attr);
+	}
+}
+
 /*
  * the object has a parent, so we have to check for visibility
  *
@@ -308,16 +496,11 @@ static int aclread_get_sd_from_ldb_message(struct aclread_context *ac,
 	}
 
 	talloc_unlink(private_data, private_data->sd_cached_blob.data);
-	if (ac->added_nTSecurityDescriptor) {
-		private_data->sd_cached_blob = sd_element->values[0];
-		talloc_steal(private_data, sd_element->values[0].data);
-	} else {
-		private_data->sd_cached_blob = ldb_val_dup(private_data,
-							   &sd_element->values[0]);
-		if (private_data->sd_cached_blob.data == NULL) {
-			TALLOC_FREE(*sd);
-			return ldb_operr(ldb);
-		}
+	private_data->sd_cached_blob = ldb_val_dup(private_data,
+						   &sd_element->values[0]);
+	if (private_data->sd_cached_blob.data == NULL) {
+		TALLOC_FREE(*sd);
+		return ldb_operr(ldb);
 	}
 
 	talloc_unlink(private_data, private_data->sd_cached);
@@ -326,6 +509,27 @@ static int aclread_get_sd_from_ldb_message(struct aclread_context *ac,
 	return LDB_SUCCESS;
 }
 
+/* Check whether the attribute is a password attribute. */
+static bool attr_is_secret(const char *attr, const struct aclread_private *private_data)
+{
+	unsigned i;
+
+	if (private_data->password_attrs == NULL) {
+		return false;
+	}
+
+	for (i = 0; private_data->password_attrs[i] != NULL; ++i) {
+		const char *password_attr = private_data->password_attrs[i];
+		if (ldb_attr_cmp(attr, password_attr) != 0) {
+			continue;
+		}
+
+		return true;
+	}
+
+	return false;
+}
+
 /*
  * Returns the access mask required to read a given attribute
  */
@@ -361,61 +565,59 @@ static uint32_t get_attr_access_mask(const struct dsdb_attribute *attr,
 	return access_mask;
 }
 
-/* helper struct for traversing the attributes in the search-tree */
-struct parse_tree_aclread_ctx {
-	struct aclread_context *ac;
-	TALLOC_CTX *mem_ctx;
-	const struct dom_sid *sid;
-	struct ldb_dn *dn;
-	struct security_descriptor *sd;
-	const struct dsdb_class *objectclass;
-	bool suppress_result;
-};
-
 /*
- * Checks that the user has sufficient access rights to view an attribute
+ * Checks that the user has sufficient access rights to view an attribute, else
+ * marks it as inaccessible.
  */
-static int check_attr_access_rights(TALLOC_CTX *mem_ctx, const char *attr_name,
-				    struct aclread_context *ac,
-				    struct security_descriptor *sd,
-				    const struct dsdb_class *objectclass,
-				    const struct dom_sid *sid, struct ldb_dn *dn)
+static int acl_redact_attr(TALLOC_CTX *mem_ctx,
+			   struct ldb_message_element *el,
+			   struct aclread_context *ac,
+			   const struct aclread_private *private_data,
+			   const struct ldb_message *msg,
+			   const struct dsdb_schema *schema,
+			   const struct security_descriptor *sd,
+			   const struct dom_sid *sid,
+			   const struct dsdb_class *objectclass)
 {
 	int ret;
 	const struct dsdb_attribute *attr = NULL;
 	uint32_t access_mask;
 	struct ldb_context *ldb = ldb_module_get_ctx(ac->module);
 
-	attr = dsdb_attribute_by_lDAPDisplayName(ac->schema, attr_name);
+	if (attr_is_secret(el->name, private_data)) {
+		ldb_msg_element_mark_inaccessible(el);
+		return LDB_SUCCESS;
+	}
+
+	/* Look up the attribute in the schema. */
+	attr = dsdb_attribute_by_lDAPDisplayName(schema, el->name);
 	if (!attr) {
 		ldb_debug_set(ldb,
-			      LDB_DEBUG_TRACE,
-			      "acl_read: %s cannot find attr[%s] in schema,"
-			      "ignoring\n",
-			      ldb_dn_get_linearized(dn), attr_name);
-		return LDB_SUCCESS;
+			      LDB_DEBUG_FATAL,
+			      "acl_read: %s cannot find attr[%s] in schema\n",
+			      ldb_dn_get_linearized(msg->dn), el->name);
+		return LDB_ERR_OPERATIONS_ERROR;
 	}
 
 	access_mask = get_attr_access_mask(attr, ac->sd_flags);
-
-	/* the access-mask should be non-zero. Skip attribute otherwise */
 	if (access_mask == 0) {
 		DBG_ERR("Could not determine access mask for attribute %s\n",
-			attr_name);
+			el->name);
+		ldb_msg_element_mark_inaccessible(el);
 		return LDB_SUCCESS;
 	}
 
+	/* We must check whether the user has rights to view the attribute. */
+
 	ret = acl_check_access_on_attribute(ac->module, mem_ctx, sd, sid,
 					    access_mask, attr, objectclass);
 
 	if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) {
-		return ret;
-	}
-
-	if (ret != LDB_SUCCESS) {
+		ldb_msg_element_mark_inaccessible(el);
+	} else if (ret != LDB_SUCCESS) {
 		ldb_debug_set(ldb, LDB_DEBUG_FATAL,
 			      "acl_read: %s check attr[%s] gives %s - %s\n",
-			      ldb_dn_get_linearized(dn), attr_name,
+			      ldb_dn_get_linearized(msg->dn), el->name,
 			      ldb_strerror(ret), ldb_errstring(ldb));
 		return ret;
 	}
@@ -423,38 +625,6 @@ static int check_attr_access_rights(TALLOC_CTX *mem_ctx, const char *attr_name,
 	return LDB_SUCCESS;
 }
 
-/*
- * Returns the attribute name for this particular level of a search operation
- * parse-tree.
- */
-static const char * parse_tree_get_attr(struct ldb_parse_tree *tree)
-{
-	const char *attr = NULL;
-
-	switch (tree->operation) {
-	case LDB_OP_EQUALITY:
-	case LDB_OP_GREATER:
-	case LDB_OP_LESS:
-	case LDB_OP_APPROX:
-		attr = tree->u.equality.attr;
-		break;
-	case LDB_OP_SUBSTRING:
-		attr = tree->u.substring.attr;
-		break;
-	case LDB_OP_PRESENT:
-		attr = tree->u.present.attr;
-		break;
-	case LDB_OP_EXTENDED:
-		attr = tree->u.extended.attr;
-		break;
-
-	/* we'll check LDB_OP_AND/_OR/_NOT children later on in the walk */
-	default:
-		break;
-	}
-	return attr;
-}
-
 static int setup_access_check_context(struct aclread_context *ac,
 				      const struct ldb_message *msg,
 				      struct access_check_context *ctx)
@@ -518,103 +688,6 @@ static int setup_access_check_context(struct aclread_context *ac,
 	return LDB_SUCCESS;
 }
 
-/*
- * Checks a single attribute in the search parse-tree to make sure the user has
- * sufficient rights to view it.
- */
-static int parse_tree_check_attr_access(struct ldb_parse_tree *tree,
-					void *private_context)
-{
-	struct parse_tree_aclread_ctx *ctx = NULL;
-	const char *attr_name = NULL;
-	int ret;
-	static const char * const attrs_always_present[] = {
-		"objectClass",
-		"distinguishedName",
-		"name",
-		"objectGUID",
-		NULL
-	};
-
-	ctx = (struct parse_tree_aclread_ctx *)private_context;
-
-	/*
-	 * we can skip any further checking if we already know that this object
-	 * shouldn't be visible in this user's search
-	 */
-	if (ctx->suppress_result) {
-		return LDB_SUCCESS;
-	}
-
-	/* skip this level of the search-tree if it has no attribute to check */
-	attr_name = parse_tree_get_attr(tree);
-	if (attr_name == NULL) {
-		return LDB_SUCCESS;
-	}
-
-	/*
-	 * If the search filter is checking for an attribute's presence, and the
-	 * attribute is always present, we can skip access rights checks. Every
-	 * object has these attributes, and so there's no security reason to
-	 * hide their presence.
-	 * Note: the acl.py tests (e.g. test_search1()) rely on this exception.
-	 * I.e. even if we lack Read Property (RP) rights for a child object, it
-	 * should still appear as a visible object in 'objectClass=*' searches,
-	 * so long as we have List Contents (LC) rights for the object.
-	 */
-	if (tree->operation == LDB_OP_PRESENT &&
-	    is_attr_in_list(attrs_always_present, attr_name)) {
-		return LDB_SUCCESS;
-	}
-
-	ret = check_attr_access_rights(ctx->mem_ctx, attr_name, ctx->ac,
-				       ctx->sd, ctx->objectclass, ctx->sid,
-				       ctx->dn);
-
-	/*
-	 * if the user does not have the rights to view this attribute, then we
-	 * should not return the object as a search result, i.e. act as if the
-	 * object doesn't exist (for this particular user, at least)
-	 */
-	if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) {
-		ctx->suppress_result = true;
-		return LDB_SUCCESS;
-	}
-
-	return ret;
-}
-
-/*
- * Traverse the search-tree to check that the user has sufficient access rights
- * to view all the attributes.
- */
-static int check_search_ops_access(struct aclread_context *ac,
-				   TALLOC_CTX *mem_ctx,
-				   struct security_descriptor *sd,
-				   const struct dsdb_class *objectclass,
-				   const struct dom_sid *sid, struct ldb_dn *dn,
-				   bool *suppress_result)
-{
-	int ret;
-	struct parse_tree_aclread_ctx ctx = { 0 };
-	struct ldb_parse_tree *tree = ac->req->op.search.tree;
-
-	ctx.ac = ac;
-	ctx.mem_ctx = mem_ctx;
-	ctx.suppress_result = false;
-	ctx.sid = sid;
-	ctx.dn = dn;
-	ctx.sd = sd;
-	ctx.objectclass = objectclass;
-
-	/* walk the search tree, checking each attribute as we go */
-	ret = ldb_parse_tree_walk(tree, parse_tree_check_attr_access, &ctx);
-
-	/* return whether this search result should be hidden to this user */
-	*suppress_result = ctx.suppress_result;
-	return ret;
-}
-
 /*
  * Whether this attribute was added to perform access checks and must be
  * removed.
@@ -650,17 +723,14 @@ static bool should_remove_attr(const char *attr, const struct aclread_context *a
 
 static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
 {
-	struct ldb_context *ldb;
 	struct aclread_context *ac;
+	struct aclread_private *private_data = NULL;
 	struct ldb_message *msg;
 	int ret;
 	unsigned int i;
 	struct access_check_context acl_ctx;
-	TALLOC_CTX *tmp_ctx;
-	bool suppress_result = false;
 
 	ac = talloc_get_type_abort(req->context, struct aclread_context);
-	ldb = ldb_module_get_ctx(ac->module);
 	if (!ares) {
 		return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR );
 	}
@@ -668,14 +738,9 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
 		return ldb_module_done(ac->req, ares->controls,
 				       ares->response, ares->error);
 	}
-	tmp_ctx = talloc_new(ac);
 	switch (ares->type) {
 	case LDB_REPLY_ENTRY:
 		msg = ares->message;
-		ret = setup_access_check_context(ac, msg, &acl_ctx);
-		if (ret != LDB_SUCCESS) {
-			return ret;
-		}
 
 		if (!ldb_dn_is_null(msg->dn)) {
 			/*
@@ -684,132 +749,88 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
 			 */
 			ret = aclread_check_object_visible(ac, msg, req);
 			if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) {
-				talloc_free(tmp_ctx);
 				return LDB_SUCCESS;
 			} else if (ret != LDB_SUCCESS) {
+				struct ldb_context *ldb = ldb_module_get_ctx(ac->module);
 				ldb_debug_set(ldb, LDB_DEBUG_FATAL,
 					      "acl_read: %s check parent %s - %s\n",
 					      ldb_dn_get_linearized(msg->dn),
 					      ldb_strerror(ret),
 					      ldb_errstring(ldb));
-				goto fail;
+				return ldb_module_done(ac->req, NULL, NULL, ret);
 			}
 		}
 
 		/* for every element in the message check RP */
-		for (i=0; i < msg->num_elements; i++) {
-			const struct dsdb_attribute *attr;
-			uint32_t access_mask;
-			attr = dsdb_attribute_by_lDAPDisplayName(ac->schema,
-								 msg->elements[i].name);
-			if (!attr) {
-				ldb_debug_set(ldb, LDB_DEBUG_FATAL,
-					      "acl_read: %s cannot find attr[%s] in of schema\n",
-					      ldb_dn_get_linearized(msg->dn),
-					      msg->elements[i].name);
-				ret = LDB_ERR_OPERATIONS_ERROR;
-				goto fail;
-			}
+		for (i = 0; i < msg->num_elements; ++i) {
+			struct ldb_message_element *el = &msg->elements[i];
+
 			/* Remove attributes added to perform access checks. */
-			if (should_remove_attr(msg->elements[i].name, ac)) {
-				ldb_msg_element_mark_inaccessible(&msg->elements[i]);
+			if (should_remove_attr(el->name, ac)) {
+				ldb_msg_element_mark_inaccessible(el);
 				continue;
 			}
 
-			access_mask = get_attr_access_mask(attr, ac->sd_flags);
-
-			if (access_mask == 0) {
-				ldb_msg_element_mark_inaccessible(&msg->elements[i]);
+			if (acl_element_is_access_checked(el)) {
+				/* We will have already checked this attribute. */
 				continue;
 			}
 
-			ret = acl_check_access_on_attribute(ac->module,
-							    tmp_ctx,
-							    acl_ctx.sd,
-							    acl_ctx.sid,
-							    access_mask,
-							    attr,
-							    objectclass);
-
 			/*
-			 * Dirsync control needs the replpropertymetadata attribute
-			 * so return it as it will be removed by the control
-			 * in anycase.
+			 * We need to fetch the security descriptor to check
+			 * this attribute.
 			 */
-			if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) {
-				bool in_search_filter;
-
-				/* check if attr is part of the search filter */
-				in_search_filter = dsdb_attr_in_parse_tree(ac->req->op.search.tree,
-								msg->elements[i].name);
-
-				if (in_search_filter) {
-
-					/*
-					 * We are doing dirysnc answers
-					 * and the object shouldn't be returned (normally)
-					 * but we will return it without replPropertyMetaData
-					 * so that the dirysync module will do what is needed
-					 * (remove the object if it is not deleted, or return
-					 * just the objectGUID if it's deleted).
-					 */
-					if (ac->indirsync) {
-						ldb_msg_remove_attr(msg, "replPropertyMetaData");
-						break;
-					} else {
-
-						/* do not return this entry */
-						talloc_free(tmp_ctx);
-						return LDB_SUCCESS;
-					}
-				} else {
-					ldb_msg_element_mark_inaccessible(&msg->elements[i]);
-				}
-			} else if (ret != LDB_SUCCESS) {
-				ldb_debug_set(ldb, LDB_DEBUG_FATAL,
-					      "acl_read: %s check attr[%s] gives %s - %s\n",
-					      ldb_dn_get_linearized(msg->dn),
-					      msg->elements[i].name,
-					      ldb_strerror(ret),
-					      ldb_errstring(ldb));
-				goto fail;
-			}
+			break;
 		}
 
-		/*
-		 * check access rights for the search attributes, as well as the
-		 * attribute values actually being returned
-		 */
-		ret = check_search_ops_access(ac, tmp_ctx, acl_ctx.sd, acl_ctx.objectclass, acl_ctx.sid,
-					      msg->dn, &suppress_result);
+		if (i == msg->num_elements) {
+			/* All elements have been checked. */
+			goto reply_entry_done;
+		}
+
+		ret = setup_access_check_context(ac, msg, &acl_ctx);
 		if (ret != LDB_SUCCESS) {
-			ldb_debug_set(ldb, LDB_DEBUG_FATAL,
-				      "acl_read: %s check search ops %s - %s\n",
-				      ldb_dn_get_linearized(msg->dn),
-				      ldb_strerror(ret), ldb_errstring(ldb));
-			goto fail;
+			return ret;
 		}
 
-		if (suppress_result) {
+		private_data = talloc_get_type_abort(ldb_module_get_private(ac->module),
+						     struct aclread_private);
+
+		for (/* begin where we left off */; i < msg->num_elements; ++i) {
+			struct ldb_message_element *el = &msg->elements[i];
+
+			/* Remove attributes added to perform access checks. */
+			if (should_remove_attr(el->name, ac)) {
+				ldb_msg_element_mark_inaccessible(el);
+				continue;
+			}
+
+			if (acl_element_is_access_checked(el)) {
+				/* We will have already checked this attribute. */
+				continue;
+			}
 
 			/*
-			 * As per the above logic, we strip replPropertyMetaData
-			 * out of the msg so that the dirysync module will do
-			 * what is needed (return just the objectGUID if it's,
-			 * deleted, or remove the object if it is not).
+			 * We need to check whether the attribute is secret,
+			 * confidential, or access-controlled.
 			 */
-			if (ac->indirsync) {
-				ldb_msg_remove_attr(msg, "replPropertyMetaData");
-			} else {
-				talloc_free(tmp_ctx);
-				return LDB_SUCCESS;
+			ret = acl_redact_attr(ac,
+					      el,
+					      ac,
+					      private_data,
+					      msg,
+					      ac->schema,
+					      acl_ctx.sd,
+					      acl_ctx.sid,
+					      acl_ctx.objectclass);
+			if (ret != LDB_SUCCESS) {
+				return ldb_module_done(ac->req, NULL, NULL, ret);
 			}
 		}
 
+	reply_entry_done:
 		ldb_msg_remove_inaccessible(msg);
 
-		talloc_free(tmp_ctx);
-
 		ac->num_entries++;
 		return ldb_module_send_entry(ac->req, msg, ares->controls);
 	case LDB_REPLY_REFERRAL:
@@ -830,9 +851,6 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
 
 	}
 	return LDB_SUCCESS;
-fail:
-	talloc_free(tmp_ctx);
-	return ldb_module_done(ac->req, NULL, NULL, ret);
 }
 
 
@@ -843,7 +861,6 @@ static int aclread_search(struct ldb_module *module, struct ldb_request *req)
 	struct aclread_context *ac;
 	struct ldb_request *down_req;
 	struct ldb_control *as_system = ldb_request_get_control(req, LDB_CONTROL_AS_SYSTEM_OID);
-	uint32_t flags = ldb_req_get_custom_flags(req);
 	struct ldb_result *res;
 	struct aclread_private *p;
 	bool need_sd = false;
@@ -878,15 +895,6 @@ static int aclread_search(struct ldb_module *module, struct ldb_request *req)
 	}
 	ac->module = module;
 	ac->req = req;
-	ac->schema = dsdb_get_schema(ldb, req);
-	if (flags & DSDB_ACL_CHECKS_DIRSYNC_FLAG) {
-		ac->indirsync = true;
-	} else {
-		ac->indirsync = false;
-	}
-	if (!ac->schema) {
-		return ldb_operr(ldb);
-	}
 
 	attrs = req->op.search.attrs;
 	if (attrs == NULL) {
@@ -943,7 +951,7 @@ static int aclread_search(struct ldb_module *module, struct ldb_request *req)
 		ac->added_nTSecurityDescriptor = true;
 	}
 
-	ac->attrs = req->op.search.attrs;
+	ac->am_administrator = dsdb_module_am_administrator(module);
 
 	/* check accessibility of base */
 	if (!ldb_dn_is_null(req->op.search.base)) {
@@ -987,19 +995,270 @@ static int aclread_search(struct ldb_module *module, struct ldb_request *req)
 		return LDB_ERR_OPERATIONS_ERROR;
 	}
 
+	/*
+	 * We provide 'ac' as the control value, which is then used by the
+	 * callback to avoid double-work.
+	 */
+	ret = ldb_request_add_control(down_req, DSDB_CONTROL_ACL_READ_OID, false, ac);
+	if (ret != LDB_SUCCESS) {
+			return ldb_error(ldb, ret,
+					"acl_read: Error adding acl_read control.");
+	}
+
 	return ldb_next_request(module, down_req);
 }
 
+/*
+ * Here we mark inaccessible attributes known to be looked for in the
+ * filter. This only redacts attributes found in the search expression. If any
+ * extended attribute match rules examine different attributes without their own
+ * access control checks, a security bypass is possible.
+ */
+static int acl_redact_msg_for_filter(struct ldb_module *module, struct ldb_request *req, struct ldb_message *msg)
+{
+	struct ldb_context *ldb = ldb_module_get_ctx(module);
+	const struct aclread_private *private_data = NULL;
+	struct ldb_control *control = NULL;
+	struct aclread_context *ac = NULL;
+	struct access_check_context acl_ctx;
+	int ret;
+	unsigned i;
+
+	/*
+	 * The private data contains a list of attributes which are to be
+	 * considered secret.
+	 */
+	private_data = talloc_get_type(ldb_module_get_private(module), struct aclread_private);
+	if (private_data == NULL) {
+		return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR,
+				 "aclread_private data is missing");
+	}
+	if (!private_data->enabled) {
+		return LDB_SUCCESS;
+	}
+
+	control = ldb_request_get_control(req, DSDB_CONTROL_ACL_READ_OID);
+	if (control == NULL) {
+		/*
+		 * We've bypassed the acl_read module for this request, and
+		 * should skip redaction in this case.
+		 */
+		return LDB_SUCCESS;
+	}
+
+	ac = talloc_get_type_abort(control->data, struct aclread_context);
+
+	if (!ac->got_tree_attrs) {
+		ret = ldb_parse_tree_collect_acl_attrs(module, ac, &ac->tree_attrs, req->op.search.tree);
+		if (ret != LDB_SUCCESS) {
+			return ret;
+		}
+		ac->got_tree_attrs = true;
+	}
+
+	for (i = 0; i < msg->num_elements; ++i) {
+		struct ldb_message_element *el = &msg->elements[i];
+
+		/* Is the attribute mentioned in the search expression? */
+		if (attr_in_vec(&ac->tree_attrs, el->name)) {
+			/*
+			 * We need to fetch the security descriptor to check
+			 * this element.
+			 */
+			break;
+		}
+
+		/*
+		 * This attribute is not in the search filter, so we can leave
+		 * handling it till aclread_callback(), by which time we know
+		 * this object is a match. This saves work checking ACLs if the
+		 * search is unindexed and most objects don't match the filter.
+		 */
+	}
+
+	if (i == msg->num_elements) {
+		/* All elements have been checked. */
+		return LDB_SUCCESS;
+	}
+
+	ret = setup_access_check_context(ac, msg, &acl_ctx);
+	if (ret != LDB_SUCCESS) {
+		return ret;
+	}
+
+	/* For every element in the message and the parse tree, check RP. */
+
+	for (/* begin where we left off */; i < msg->num_elements; ++i) {
+		struct ldb_message_element *el = &msg->elements[i];
+
+		/* Is the attribute mentioned in the search expression? */
+		if (!attr_in_vec(&ac->tree_attrs, el->name)) {
+			/*
+			 * If not, leave it for later and check the next
+			 * attribute.
+			 */
+			continue;
+		}
+
+		/*
+		 * We need to check whether the attribute is secret,
+		 * confidential, or access-controlled.
+		 */
+		ret = acl_redact_attr(ac,
+				      el,
+				      ac,
+				      private_data,
+				      msg,
+				      ac->schema,
+				      acl_ctx.sd,
+				      acl_ctx.sid,
+				      acl_ctx.objectclass);
+		if (ret != LDB_SUCCESS) {
+			return ret;
+		}
+
+		acl_element_mark_access_checked(el);
+	}
+
+	return LDB_SUCCESS;
+}
+
 static int aclread_init(struct ldb_module *module)
 {
 	struct ldb_context *ldb = ldb_module_get_ctx(module);
+	unsigned int i, n, j;
+	TALLOC_CTX *mem_ctx = NULL;
+	int ret;
+	bool userPassword_support;
+	static const char * const attrs[] = { "passwordAttribute", NULL };
+	static const char * const secret_attrs[] = {
+		DSDB_SECRET_ATTRIBUTES
+	};
+	struct ldb_result *res;
+	struct ldb_message *msg;
+	struct ldb_message_element *password_attributes;
 	struct aclread_private *p = talloc_zero(module, struct aclread_private);
 	if (p == NULL) {
 		return ldb_module_oom(module);
 	}
 	p->enabled = lpcfg_parm_bool(ldb_get_opaque(ldb, "loadparm"), NULL, "acl", "search", true);
+
+	ret = ldb_mod_register_control(module, LDB_CONTROL_SD_FLAGS_OID);
+	if (ret != LDB_SUCCESS) {
+		ldb_debug(ldb, LDB_DEBUG_ERROR,
+			  "acl_module_init: Unable to register sd_flags control with rootdse!\n");
+		return ldb_operr(ldb);
+	}
+
 	ldb_module_set_private(module, p);
-	return ldb_next_init(module);
+
+	mem_ctx = talloc_new(module);
+	if (!mem_ctx) {
+		return ldb_oom(ldb);
+	}
+
+	ret = dsdb_module_search_dn(module, mem_ctx, &res,
+				    ldb_dn_new(mem_ctx, ldb, "@KLUDGEACL"),
+				    attrs,
+				    DSDB_FLAG_NEXT_MODULE |
+				    DSDB_FLAG_AS_SYSTEM,
+				    NULL);
+	if (ret != LDB_SUCCESS) {
+		goto done;
+	}
+	if (res->count == 0) {
+		goto done;
+	}
+
+	if (res->count > 1) {
+		talloc_free(mem_ctx);
+		return LDB_ERR_CONSTRAINT_VIOLATION;
+	}
+
+	msg = res->msgs[0];
+
+	password_attributes = ldb_msg_find_element(msg, "passwordAttribute");
+	if (!password_attributes) {
+		goto done;
+	}
+	p->password_attrs = talloc_array(p, const char *,
+			password_attributes->num_values +
+			ARRAY_SIZE(secret_attrs) + 1);
+	if (!p->password_attrs) {
+		talloc_free(mem_ctx);
+		return ldb_oom(ldb);
+	}
+
+	n = 0;
+	for (i=0; i < password_attributes->num_values; i++) {
+		p->password_attrs[n] = (const char *)password_attributes->values[i].data;
+		talloc_steal(p->password_attrs, password_attributes->values[i].data);
+		n++;
+	}
+
+	for (i=0; i < ARRAY_SIZE(secret_attrs); i++) {
+		bool found = false;
+
+		for (j=0; j < n; j++) {
+			if (strcasecmp(p->password_attrs[j], secret_attrs[i]) == 0) {
+				found = true;
+				break;
+			}
+		}
+
+		if (found) {
+			continue;
+		}
+
+		p->password_attrs[n] = talloc_strdup(p->password_attrs,
+						     secret_attrs[i]);
+		if (p->password_attrs[n] == NULL) {
+			talloc_free(mem_ctx);
+			return ldb_oom(ldb);
+		}
+		n++;
+	}
+	p->password_attrs[n] = NULL;
+
+	ret = ldb_register_redact_callback(ldb, acl_redact_msg_for_filter, module);
+	if (ret != LDB_SUCCESS) {
+		return ret;
+	}
+
+done:
+	talloc_free(mem_ctx);
+	ret = ldb_next_init(module);
+
+	if (ret != LDB_SUCCESS) {
+		return ret;
+	}
+
+	if (p->password_attrs != NULL) {
+		/*
+		 * Check this after the modules have be initialised so we can
+		 * actually read the backend DB.
+		 */
+		userPassword_support = dsdb_user_password_support(module,
+								  module,
+								  NULL);
+		if (!userPassword_support) {
+			/*
+			 * Remove the userPassword attribute, as it is not
+			 * considered secret.
+			 */
+			for (i = 0; p->password_attrs[i] != NULL; ++i) {
+				if (ldb_attr_cmp(p->password_attrs[i], "userPassword") == 0) {
+					break;
+				}
+			}
+
+			/* Shift following elements backwards by one. */
+			for (; p->password_attrs[i] != NULL; ++i) {
+				p->password_attrs[i] = p->password_attrs[i + 1];
+			}
+		}
+	}
+	return ret;
 }
 
 static const struct ldb_module_ops ldb_aclread_module_ops = {
diff --git a/source4/dsdb/samdb/samdb.h b/source4/dsdb/samdb/samdb.h
index 3db7704307f..f22b4d99972 100644
--- a/source4/dsdb/samdb/samdb.h
+++ b/source4/dsdb/samdb/samdb.h
@@ -232,6 +232,8 @@ struct dsdb_control_transaction_identifier {
  */
 #define DSDB_CONTROL_FORCE_ALLOW_VALIDATED_DNS_HOSTNAME_SPN_WRITE_OID "1.3.6.1.4.1.7165.4.3.35"
 
+#define DSDB_CONTROL_ACL_READ_OID "1.3.6.1.4.1.7165.4.3.37"
+
 #define DSDB_EXTENDED_REPLICATED_OBJECTS_OID "1.3.6.1.4.1.7165.4.4.1"
 struct dsdb_extended_replicated_object {
 	struct ldb_message *msg;
diff --git a/source4/dsdb/tests/python/confidential_attr.py b/source4/dsdb/tests/python/confidential_attr.py
index 031c9690ba6..6889d5a5560 100755
--- a/source4/dsdb/tests/python/confidential_attr.py
+++ b/source4/dsdb/tests/python/confidential_attr.py
@@ -490,7 +490,7 @@ class ConfidentialAttrTest(ConfidentialAttrCommon):
         self.make_attr_confidential()
 
         self.assert_conf_attr_searches(has_rights_to=0)
-        dc_mode = self.guess_dc_mode()
+        dc_mode = DC_MODE_RETURN_ALL
         self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode)
         self.assert_attr_visible(expect_attr=False)
 
@@ -503,7 +503,7 @@ class ConfidentialAttrTest(ConfidentialAttrCommon):
         self.make_attr_confidential()
 
         self.assert_conf_attr_searches(has_rights_to=0)
-        dc_mode = self.guess_dc_mode()
+        dc_mode = DC_MODE_RETURN_ALL
         self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode)
         self.assert_attr_visible(expect_attr=False)
 
@@ -566,7 +566,7 @@ class ConfidentialAttrTest(ConfidentialAttrCommon):
         self.make_attr_confidential()
 
         self.assert_conf_attr_searches(has_rights_to=0)
-        dc_mode = self.guess_dc_mode()
+        dc_mode = DC_MODE_RETURN_ALL
         self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode)
         self.assert_attr_visible(expect_attr=False)
 
@@ -741,7 +741,7 @@ class ConfidentialAttrTestDenyAcl(ConfidentialAttrCommon):
 
         # the user shouldn't be able to see the attribute anymore
         self.assert_conf_attr_searches(has_rights_to="deny-one")
-        dc_mode = self.guess_dc_mode()
+        dc_mode = DC_MODE_RETURN_ALL
         self.assert_negative_searches(has_rights_to="deny-one",
                                       dc_mode=dc_mode)
         self.assert_attr_visible(expect_attr=False)
@@ -917,7 +917,7 @@ class ConfidentialAttrTestDirsync(ConfidentialAttrCommon):
 
         self.assert_conf_attr_searches(has_rights_to=0)
         self.assert_attr_visible(expect_attr=False)
-        dc_mode = self.guess_dc_mode()
+        dc_mode = DC_MODE_RETURN_ALL
         self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode)
 
         # as a final sanity-check, make sure the admin can still see the attr
@@ -1012,7 +1012,7 @@ class ConfidentialAttrTestDirsync(ConfidentialAttrCommon):
         # check we can't see the objects now, even with using dirsync controls
         self.assert_conf_attr_searches(has_rights_to=0)
         self.assert_attr_visible(expect_attr=False)
-        dc_mode = self.guess_dc_mode()
+        dc_mode = DC_MODE_RETURN_ALL
         self.assert_negative_searches(has_rights_to=0, dc_mode=dc_mode)
 
         # now delete the users (except for the user whose LDB connection
diff --git a/source4/setup/schema_samba4.ldif b/source4/setup/schema_samba4.ldif
index 79800bfd6df..27c7e9dbb47 100644
--- a/source4/setup/schema_samba4.ldif
+++ b/source4/setup/schema_samba4.ldif
@@ -233,6 +233,7 @@
 #Allocated: DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID 1.3.6.1.4.1.7165.4.3.34
 #Allocated: DSDB_CONTROL_FORCE_ALLOW_VALIDATED_DNS_HOSTNAME_SPN_WRITE_OID 1.3.6.1.4.1.7165.4.3.35
 #Allocated: DSDB_CONTROL_CALCULATED_DEFAULT_SD_OID 1.3.6.1.4.1.7165.4.3.36
+#Allocated: DSDB_CONTROL_ACL_READ_OID 1.3.6.1.4.1.7165.4.3.37
 
 
 # Extended 1.3.6.1.4.1.7165.4.4.x
-- 
2.25.1