sudo/plugins/sudoers/ldap_innetgr.c

265 lines
7.4 KiB
C

/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2023 Todd C. Miller <Todd.Miller@sudo.ws>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* This is an open source non-commercial project. Dear PVS-Studio, please check it.
* PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
*/
#include <config.h>
#include <sys/types.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif /* HAVE_STRINGS_H */
#include <time.h>
#include <ctype.h>
#ifdef HAVE_LBER_H
# include <lber.h>
#endif
#include <ldap.h>
#include <sudoers.h>
#include <sudo_ldap.h>
#include <sudo_ldap_conf.h>
/*
* Compare str to netgroup string ngstr of length nglen where str is a
* NUL-terminated string and ngstr is part of a netgroup triple string.
* Uses innetgr(3)-style matching rules.
* Returns true if the strings match, else false.
*/
static bool
sudo_ldap_netgroup_match_str(const char *str, const char *ngstr, size_t nglen,
bool ignore_case)
{
debug_decl(sudo_ldap_netgroup_match_str, SUDOERS_DEBUG_LDAP);
/* Skip leading whitespace. */
while (isspace((unsigned char)*ngstr) && nglen > 0) {
ngstr++;
nglen--;
}
/* Skip trailing whitespace. */
while (nglen > 0 && isspace((unsigned char)ngstr[nglen - 1])) {
nglen--;
}
sudo_debug_printf(SUDO_DEBUG_DEBUG, "%s: compare \"%s\" to \"%.*s\"",
__func__, str ? str : "", (int)nglen, ngstr);
if (nglen == 0 || str == NULL) {
/* An empty string is a wildcard. */
debug_return_bool(true);
}
if (*ngstr == '-' && nglen == 1) {
/* '-' means no valid value. */
debug_return_bool(false);
}
if (ignore_case) {
if (strncasecmp(str, ngstr, nglen) == 0 && str[nglen] == '\0')
debug_return_bool(true);
} else {
if (strncmp(str, ngstr, nglen) == 0 && str[nglen] == '\0')
debug_return_bool(true);
}
debug_return_bool(false);
}
/*
* Match the specified netgroup triple using the given host,
* user and domain. Matching rules as per innetgr(3).
* Returns 1 on match, else 0.
*/
static int
sudo_ldap_match_netgroup(const char *triple, const char *host,
const char *user, const char *domain)
{
const char *cp, *ep;
debug_decl(sudo_ldap_match_netgroup, SUDOERS_DEBUG_LDAP);
/* Trim leading space, check for opening paren. */
while (isspace((unsigned char)*triple))
triple++;
if (*triple != '(') {
sudo_debug_printf(SUDO_DEBUG_ERROR, "%s: invalid triple: %s",
__func__, triple);
debug_return_int(0);
}
sudo_debug_printf(SUDO_DEBUG_INFO, "%s: matching (%s,%s,%s) against %s",
__func__, host ? host : "", user ? user : "", domain ? domain : "",
triple);
/* Parse host. */
cp = triple + 1;
ep = strchr(cp, ',');
if (ep == NULL || !sudo_ldap_netgroup_match_str(host, cp, (size_t)(ep - cp), true))
debug_return_int(0);
/* Parse user. */
cp = ep + 1;
ep = strchr(cp, ',');
if (ep == NULL || !sudo_ldap_netgroup_match_str(user, cp, (size_t)(ep - cp), def_case_insensitive_user))
debug_return_int(0);
/* Parse domain. */
cp = ep + 1;
ep = strchr(cp, ')');
if (ep == NULL || !sudo_ldap_netgroup_match_str(domain, cp, (size_t)(ep - cp), true))
debug_return_int(0);
debug_return_int(1);
}
#define MAX_NETGROUP_DEPTH 128
struct netgroups_seen {
const char *groups[MAX_NETGROUP_DEPTH];
size_t len;
};
static int
sudo_ldap_innetgr_base(LDAP *ld, const char *base,
struct timeval *timeout, const char *netgr, const char *host,
const char *user, const char *domain, struct netgroups_seen *seen)
{
char *escaped_netgr = NULL, *filt = NULL;
LDAPMessage *entry, *result = NULL;
int rc, ret = 0;
size_t n;
debug_decl(sudo_ldap_innetgr_base, SUDOERS_DEBUG_LDAP);
/* Cycle detection. */
for (n = 0; n < seen->len; n++) {
if (strcmp(netgr, seen->groups[n]) == 0) {
DPRINTF1("%s: cycle in netgroups", netgr);
goto done;
}
}
if (seen->len + 1 > MAX_NETGROUP_DEPTH) {
DPRINTF1("%s: too many nested netgroups", netgr);
goto done;
}
seen->groups[seen->len++] = netgr;
/* Escape the netgroup name per RFC 4515. */
if ((escaped_netgr = sudo_ldap_value_dup(netgr)) == NULL)
goto done;
/* Build nisNetgroup query. */
rc = asprintf(&filt, "(&%s(cn=%s))",
ldap_conf.netgroup_search_filter, escaped_netgr);
if (rc == -1)
goto done;
DPRINTF1("ldap netgroup search filter: '%s'", filt);
/* Perform an LDAP query for nisNetgroup. */
DPRINTF1("searching from netgroup_base '%s'", base);
rc = ldap_search_ext_s(ld, base, LDAP_SCOPE_SUBTREE, filt,
NULL, 0, NULL, NULL, timeout, 0, &result);
free(filt);
if (rc != LDAP_SUCCESS) {
DPRINTF1("ldap netgroup search failed: %s", ldap_err2string(rc));
goto done;
}
LDAP_FOREACH(entry, ld, result) {
struct berval **bv, **p;
/* Check all nisNetgroupTriple entries. */
bv = ldap_get_values_len(ld, entry, "nisNetgroupTriple");
if (bv == NULL) {
const int optrc = ldap_get_option(ld, LDAP_OPT_RESULT_CODE, &rc);
if (optrc != LDAP_OPT_SUCCESS || rc == LDAP_NO_MEMORY)
goto done;
} else {
for (p = bv; *p != NULL && !ret; p++) {
char *val = (*p)->bv_val;
if (sudo_ldap_match_netgroup(val, host, user, domain)) {
ret = 1;
break;
}
}
ldap_value_free_len(bv);
if (ret == 1)
break;
}
/* Handle nested netgroups. */
bv = ldap_get_values_len(ld, entry, "memberNisNetgroup");
if (bv == NULL) {
const int optrc = ldap_get_option(ld, LDAP_OPT_RESULT_CODE, &rc);
if (optrc != LDAP_OPT_SUCCESS || rc == LDAP_NO_MEMORY)
goto done;
} else {
for (p = bv; *p != NULL && !ret; p++) {
const char *val = (*p)->bv_val;
const size_t saved_len = seen->len;
ret = sudo_ldap_innetgr_base(ld, base, timeout, val, host,
user, domain, seen);
/* Restore seen state to avoid use-after-free. */
seen->len = saved_len;
}
ldap_value_free_len(bv);
}
}
done:
ldap_msgfree(result);
free(escaped_netgr);
debug_return_int(ret);
}
int
sudo_ldap_innetgr_int(void *v, const char *netgr, const char *host,
const char *user, const char *domain)
{
LDAP *ld = v;
struct timeval tv, *tvp = NULL;
struct ldap_config_str *base;
struct netgroups_seen seen;
int ret = 0;
debug_decl(sudo_ldap_innetgr, SUDOERS_DEBUG_LDAP);
if (STAILQ_EMPTY(&ldap_conf.netgroup_base)) {
/* LDAP netgroups not configured. */
debug_return_int(-1);
}
if (ldap_conf.timeout > 0) {
tv.tv_sec = ldap_conf.timeout;
tv.tv_usec = 0;
tvp = &tv;
}
/* Perform an LDAP query for nisNetgroup. */
STAILQ_FOREACH(base, &ldap_conf.netgroup_base, entries) {
seen.len = 0;
ret = sudo_ldap_innetgr_base(ld, base->val, tvp, netgr, host,
user, domain, &seen);
if (ret != 0)
break;
}
debug_return_int(ret);
}