From e88087721be391ec851b3cad8a88a5476f03d317 Mon Sep 17 00:00:00 2001
From: "Todd C. Miller" <Todd.Miller@sudo.ws>
Date: Tue, 18 Jan 2022 11:20:22 -0700
Subject: [PATCH] Add support in the LDAP filter for negated users. Based on a
 diff from Simon Lees

---
 docs/sudoers.ldap.man.in  |  31 ++++------
 docs/sudoers.ldap.mdoc.in |  28 ++++-----
 plugins/sudoers/ldap.c    | 116 ++++++++++++++++++++++++++++----------
 3 files changed, 109 insertions(+), 66 deletions(-)

Index: sudo-1.9.5p2/plugins/sudoers/ldap.c
===================================================================
--- sudo-1.9.5p2.orig/plugins/sudoers/ldap.c
+++ sudo-1.9.5p2/plugins/sudoers/ldap.c
@@ -1,7 +1,7 @@
 /*
  * SPDX-License-Identifier: ISC
  *
- * Copyright (c) 2003-2020 Todd C. Miller <Todd.Miller@sudo.ws>
+ * Copyright (c) 2003-2022 Todd C. Miller <Todd.Miller@sudo.ws>
  *
  * This code is derived from software contributed by Aaron Spangler.
  *
@@ -315,18 +315,18 @@ sudo_ldap_get_values_len(LDAP *ld, LDAPM
 /*
  * Walk through search results and return true if we have a matching
  * non-Unix group (including netgroups), else false.
+ * A matching entry that is negated will always return false.
  */
 static int
 sudo_ldap_check_non_unix_group(LDAP *ld, LDAPMessage *entry, struct passwd *pw)
 {
     struct berval **bv, **p;
     bool ret = false;
-    char *val;
     int rc;
     debug_decl(sudo_ldap_check_non_unix_group, SUDOERS_DEBUG_LDAP);
 
     if (!entry)
-	debug_return_bool(ret);
+	debug_return_bool(false);
 
     /* get the values from the entry */
     bv = sudo_ldap_get_values_len(ld, entry, "sudoUser", &rc);
@@ -338,18 +338,29 @@ sudo_ldap_check_non_unix_group(LDAP *ld,
 
     /* walk through values */
     for (p = bv; *p != NULL && !ret; p++) {
-	val = (*p)->bv_val;
+	bool negated = false;
+	char *val = (*p)->bv_val;
+
+	if (*val == '!') {
+	    val++;
+	    negated = true;
+	}
 	if (*val == '+') {
 	    if (netgr_matches(val, def_netgroup_tuple ? user_runhost : NULL,
 		def_netgroup_tuple ? user_srunhost : NULL, pw->pw_name))
 		ret = true;
-	    DPRINTF2("ldap sudoUser netgroup '%s' ... %s", val,
-		ret ? "MATCH!" : "not");
+	    DPRINTF2("ldap sudoUser netgroup '%s%s' ... %s",
+		negated ? "!" : "", val, ret ? "MATCH!" : "not");
 	} else {
 	    if (group_plugin_query(pw->pw_name, val + 2, pw))
 		ret = true;
-	    DPRINTF2("ldap sudoUser non-Unix group '%s' ... %s", val,
-		ret ? "MATCH!" : "not");
+	    DPRINTF2("ldap sudoUser non-Unix group '%s%s' ... %s",
+		negated ? "!" : "", val, ret ? "MATCH!" : "not");
+	}
+	/* A negated match overrides all other entries. */
+	if (ret && negated) {
+	    ret = false;
+	    break;
 	}
     }
 
@@ -923,7 +934,8 @@ done:
 static char *
 sudo_ldap_build_pass1(LDAP *ld, struct passwd *pw)
 {
-    char *buf, timebuffer[TIMEFILTER_LENGTH + 1], idbuf[MAX_UID_T_LEN + 1];
+    char timebuffer[TIMEFILTER_LENGTH + 1], idbuf[MAX_UID_T_LEN + 1];
+    char *buf, *notbuf;
     struct ldap_netgroup_list netgroups;
     struct ldap_netgroup *ng = NULL;
     struct gid_list *gidlist;
@@ -935,34 +947,45 @@ sudo_ldap_build_pass1(LDAP *ld, struct p
 
     STAILQ_INIT(&netgroups);
 
-    /* If there is a filter, allocate space for the global AND. */
-    if (ldap_conf.timed || ldap_conf.search_filter)
+    if (ldap_conf.timed || ldap_conf.search_filter) {
+	/* Allocate space for the global AND. */
 	sz += 3;
 
-    /* Add LDAP search filter if present. */
-    if (ldap_conf.search_filter)
-	sz += strlen(ldap_conf.search_filter);
+	/* Add LDAP search filter if present. */
+	if (ldap_conf.search_filter)
+	    sz += strlen(ldap_conf.search_filter);
+
+	/* If timed, add space for time limits. */
+	if (ldap_conf.timed)
+	    sz += TIMEFILTER_LENGTH;
+    }
+
+    /* Add space for the global OR clause + (sudoUser=ALL) + NOT + NUL. */
+    sz += sizeof("(|(sudoUser=ALL)(!(|)))");
 
-    /* Then add (|(sudoUser=USERNAME)(sudoUser=#uid)(sudoUser=ALL)) + NUL */
-    sz += 29 + (12 + MAX_UID_T_LEN) + sudo_ldap_value_len(pw->pw_name);
+    /* Add space for username and uid, including the negated versions. */
+    sz += ((sizeof("(sudoUser=)(sudoUser=#)") - 1 +
+	sudo_ldap_value_len(pw->pw_name) + MAX_UID_T_LEN) * 2) + 2;
 
     /* Add space for primary and supplementary groups and gids */
     if ((grp = sudo_getgrgid(pw->pw_gid)) != NULL) {
-	sz += 12 + sudo_ldap_value_len(grp->gr_name);
+	sz += ((sizeof("(sudoUser=%)") - 1 +
+	    sudo_ldap_value_len(grp->gr_name)) * 2) + 1;
     }
-    sz += 13 + MAX_UID_T_LEN;
+    sz += ((sizeof("(sudoUser=%#)") - 1 + MAX_UID_T_LEN) * 2) + 1;
     if ((grlist = sudo_get_grlist(pw)) != NULL) {
 	for (i = 0; i < grlist->ngroups; i++) {
 	    if (grp != NULL && strcasecmp(grlist->groups[i], grp->gr_name) == 0)
 		continue;
-	    sz += 12 + sudo_ldap_value_len(grlist->groups[i]);
+	    sz += ((sizeof("(sudoUser=%)") - 1 +
+		sudo_ldap_value_len(grlist->groups[i])) * 2) + 1;
 	}
     }
     if ((gidlist = sudo_get_gidlist(pw, ENTRY_TYPE_ANY)) != NULL) {
 	for (i = 0; i < gidlist->ngids; i++) {
 	    if (pw->pw_gid == gidlist->gids[i])
 		continue;
-	    sz += 13 + MAX_UID_T_LEN;
+	    sz += ((sizeof("(sudoUser=%#)") - 1 + MAX_UID_T_LEN) * 2) + 1;
 	}
     }
 
@@ -971,7 +994,7 @@ sudo_ldap_build_pass1(LDAP *ld, struct p
 	DPRINTF1("Looking up netgroups for %s", pw->pw_name);
 	if (sudo_netgroup_lookup(ld, pw, &netgroups)) {
 	    STAILQ_FOREACH(ng, &netgroups, entries) {
-		sz += 14 + strlen(ng->name);
+		sz += ((sizeof("(sudoUser=+)") - 1 + strlen(ng->name)) * 2) + 1;
 	    }
 	} else {
 	    /* sudo_netgroup_lookup() failed, clean up. */
@@ -983,12 +1006,12 @@ sudo_ldap_build_pass1(LDAP *ld, struct p
 	}
     }
 
-    /* If timed, add space for time limits. */
-    if (ldap_conf.timed)
-	sz += TIMEFILTER_LENGTH;
-    if ((buf = malloc(sz)) == NULL)
+    buf = malloc(sz);
+    notbuf = malloc(sz);
+    if (buf == NULL || notbuf == NULL)
 	goto bad;
     *buf = '\0';
+    *notbuf = '\0';
 
     /*
      * If timed or using a search filter, start a global AND clause to
@@ -1004,23 +1027,35 @@ sudo_ldap_build_pass1(LDAP *ld, struct p
     CHECK_STRLCAT(buf, "(|(sudoUser=", sz);
     CHECK_LDAP_VCAT(buf, pw->pw_name, sz);
     CHECK_STRLCAT(buf, ")", sz);
+    CHECK_STRLCAT(notbuf, "(sudoUser=!", sz);
+    CHECK_LDAP_VCAT(notbuf, pw->pw_name, sz);
+    CHECK_STRLCAT(notbuf, ")", sz);
 
     /* Append user-ID */
     (void) snprintf(idbuf, sizeof(idbuf), "%u", (unsigned int)pw->pw_uid);
     CHECK_STRLCAT(buf, "(sudoUser=#", sz);
     CHECK_STRLCAT(buf, idbuf, sz);
     CHECK_STRLCAT(buf, ")", sz);
+    CHECK_STRLCAT(notbuf, "(sudoUser=!#", sz);
+    CHECK_STRLCAT(notbuf, idbuf, sz);
+    CHECK_STRLCAT(notbuf, ")", sz);
 
     /* Append primary group and group-ID */
     if (grp != NULL) {
 	CHECK_STRLCAT(buf, "(sudoUser=%", sz);
 	CHECK_LDAP_VCAT(buf, grp->gr_name, sz);
 	CHECK_STRLCAT(buf, ")", sz);
+	CHECK_STRLCAT(notbuf, "(sudoUser=!%", sz);
+	CHECK_LDAP_VCAT(notbuf, grp->gr_name, sz);
+	CHECK_STRLCAT(notbuf, ")", sz);
     }
     (void) snprintf(idbuf, sizeof(idbuf), "%u", (unsigned int)pw->pw_gid);
     CHECK_STRLCAT(buf, "(sudoUser=%#", sz);
     CHECK_STRLCAT(buf, idbuf, sz);
     CHECK_STRLCAT(buf, ")", sz);
+    CHECK_STRLCAT(notbuf, "(sudoUser=!%#", sz);
+    CHECK_STRLCAT(notbuf, idbuf, sz);
+    CHECK_STRLCAT(notbuf, ")", sz);
 
     /* Append supplementary groups and group-IDs */
     if (grlist != NULL) {
@@ -1030,6 +1065,9 @@ sudo_ldap_build_pass1(LDAP *ld, struct p
 	    CHECK_STRLCAT(buf, "(sudoUser=%", sz);
 	    CHECK_LDAP_VCAT(buf, grlist->groups[i], sz);
 	    CHECK_STRLCAT(buf, ")", sz);
+	    CHECK_STRLCAT(notbuf, "(sudoUser=!%", sz);
+	    CHECK_LDAP_VCAT(notbuf, grlist->groups[i], sz);
+	    CHECK_STRLCAT(notbuf, ")", sz);
 	}
     }
     if (gidlist != NULL) {
@@ -1041,6 +1079,9 @@ sudo_ldap_build_pass1(LDAP *ld, struct p
 	    CHECK_STRLCAT(buf, "(sudoUser=%#", sz);
 	    CHECK_STRLCAT(buf, idbuf, sz);
 	    CHECK_STRLCAT(buf, ")", sz);
+	    CHECK_STRLCAT(notbuf, "(sudoUser=!%#", sz);
+	    CHECK_STRLCAT(notbuf, idbuf, sz);
+	    CHECK_STRLCAT(notbuf, ")", sz);
 	}
     }
 
@@ -1058,12 +1099,20 @@ sudo_ldap_build_pass1(LDAP *ld, struct p
 	CHECK_STRLCAT(buf, "(sudoUser=+", sz);
 	CHECK_LDAP_VCAT(buf, ng->name, sz);
 	CHECK_STRLCAT(buf, ")", sz);
+	CHECK_STRLCAT(notbuf, "(sudoUser=!+", sz);
+	CHECK_LDAP_VCAT(notbuf, ng->name, sz);
+	CHECK_STRLCAT(notbuf, ")", sz);
 	free(ng->name);
 	free(ng);
     }
 
-    /* Add ALL to list and end the global OR. */
-    CHECK_STRLCAT(buf, "(sudoUser=ALL)", sz);
+    /* Add ALL to list. */
+    CHECK_STRLCAT(buf, "(sudoUser=ALL))", sz);
+
+    /* Add filter for negated entries. */
+    CHECK_STRLCAT(buf, "(!(|", sz);
+    CHECK_STRLCAT(buf, notbuf, sz);
+    CHECK_STRLCAT(buf, ")", sz);
 
     /* Add the time restriction, or simply end the global OR. */
     if (ldap_conf.timed) {
@@ -1074,8 +1123,10 @@ sudo_ldap_build_pass1(LDAP *ld, struct p
     } else if (ldap_conf.search_filter) {
 	CHECK_STRLCAT(buf, ")", sz); /* closes the global OR */
     }
+
     CHECK_STRLCAT(buf, ")", sz); /* closes the global OR or the global AND */
 
+    free(notbuf);
     debug_return_str(buf);
 overflow:
     sudo_warnx(U_("internal error, %s overflow"), __func__);
@@ -1092,6 +1143,7 @@ bad:
 	free(ng);
     }
     free(buf);
+    free(notbuf);
     debug_return_str(NULL);
 }
 
@@ -1128,16 +1180,18 @@ sudo_ldap_build_pass2(void)
      * those get ANDed in to the expression.
      */
     if (query_netgroups && def_group_plugin) {
-	len = asprintf(&filt, "%s%s(|(sudoUser=+*)(sudoUser=%%:*))%s%s",
+	len = asprintf(&filt, "%s%s(|(sudoUser=+*)(sudoUser=!+*)(sudoUser=%%:*)(sudoUser=!%%:*))%s%s",
 	    (ldap_conf.timed || ldap_conf.search_filter) ? "(&" : "",
 	    ldap_conf.search_filter ? ldap_conf.search_filter : "",
 	    ldap_conf.timed ? timebuffer : "",
 	    (ldap_conf.timed || ldap_conf.search_filter) ? ")" : "");
     } else {
-	len = asprintf(&filt, "(&%s(sudoUser=*)(sudoUser=%s*)%s)",
+	len = asprintf(&filt, "%s%s(|(sudoUser=%s*)(sudoUser=!%s*))%s%s",
+	    (ldap_conf.timed || ldap_conf.search_filter) ? "(&" : "",
 	    ldap_conf.search_filter ? ldap_conf.search_filter : "",
-	    query_netgroups ? "+" : "%:",
-	    ldap_conf.timed ? timebuffer : "");
+	    query_netgroups ? "+" : "%:", query_netgroups ? "+" : "%:",
+	    ldap_conf.timed ? timebuffer : "",
+	    (ldap_conf.timed || ldap_conf.search_filter) ? ")" : "");
     }
     if (len == -1)
 	filt = NULL;
