CVE-2022-4254 (C, published 2023-02-01T17:15Z)
Description: value: sssd: libsss_certmap fails to sanitise certificate data used in LDAP filters
Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-90 \u2014 Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection')
The product constructs all or part of an LDAP query using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended LDAP query when it is sent to a downstream component.
Repository: https://github.com/SSSD/sssd
Files changed:
Makefile.am
src/lib/certmap/sss_certmap.c
src/lib/certmap/sss_certmap.exports
src/lib/certmap/sss_certmap.h
src/responder/pam/pamsrv_p11.c
src/tests/cmocka/test_certmap.c
src/util/util.c
src/util/util_ext.c
Commit message: certmap: sanitize LDAP search filter

The sss_certmap_get_search_filter() will now sanitize the values read
from the certificates before adding them to a search filter. To be able
to get the plain values as well sss_certmap_expand_mapping_rule() is
added.

Resolves:
https://github.com/SSSD/sssd/issues/5135

Reviewed-by: Alexey Tikhonov <atikhono@redhat.com>


Vulnerable code:
    -version-info 1:0:1
                      struct sss_cert_content *cert_content,
                                  &expanded);
int sss_certmap_get_search_filter(struct sss_certmap_ctx *ctx,
        ret = get_filter(ctx, ctx->default_mapping_rule, cert_content, &filter);
                                 &filter);
 * @param[in] der_cert binary blog with the DER encoded certificate
 * @param[in] der_cert binary blog with the DER encoded certificate
 * @param[out] filter  LDAP filter string, caller should free the data by
 *                     calling sss_certmap_free_filter_and_domains
 * @param[in]  der_cert   binary blog with the DER encoded certificate
    ret = sss_certmap_get_search_filter(ctx, der, der_size, &filter, &domains);
        DEBUG(SSSDBG_OP_FAILURE, "sss_certmap_get_search_filter failed.\n");
    assert_string_equal(filter, "rule94=<I>O=IPA.DEVEL,CN=Certificate Authority"
errno_t sss_filter_sanitize_ex(TALLOC_CTX *mem_ctx,
                               const char *input,
                               char **sanitized,
                               const char *ignore)
{
    char *output;
    size_t i = 0;
    size_t j = 0;
    char *allowed;

    /* Assume the worst-case. We'll resize it later, once */
    output = talloc_array(mem_ctx, char, strlen(input) * 3 + 1);
    if (!output) {
        return ENOMEM;
    }

    while (input[i]) {
        /* Even though this character might have a special meaning, if it's
         * explicitly allowed, just copy it and move on
         */
        if (ignore == NULL) {
            allowed = NULL;
        } else {
            allowed = strchr(ignore, input[i]);
        }
        if (allowed) {
            output[j++] = input[i++];
            continue;
        }

        switch(input[i]) {
        case '\t':
            output[j++] = '\\';
            output[j++] = '0';
            output[j++] = '9';
            break;
        case ' ':
            output[j++] = '\\';
            output[j++] = '2';
            output[j++] = '0';
            break;
        case '*':
            output[j++] = '\\';
            output[j++] = '2';
            output[j++] = 'a';
            break;
        case '(':
            output[j++] = '\\';
            output[j++] = '2';
            output[j++] = '8';
            break;
        case ')':
            output[j++] = '\\';
            output[j++] = '2';
            output[j++] = '9';
            break;
        case '\\':
            output[j++] = '\\';
            output[j++] = '5';
            output[j++] = 'c';
            break;
        case '\r':
            output[j++] = '\\';
            output[j++] = '0';
            output[j++] = 'd';
            break;
        case '\n':
            output[j++] = '\\';
            output[j++] = '0';
            output[j++] = 'a';
            break;
        default:
            output[j++] = input[i];
        }

        i++;
    }
    output[j] = '\0';
    *sanitized = talloc_realloc(mem_ctx, output, char, j+1);
    if (!*sanitized) {
        talloc_free(output);
        return ENOMEM;
    }

    return EOK;
}

errno_t sss_filter_sanitize(TALLOC_CTX *mem_ctx,
                            const char *input,
                            char **sanitized)
{
    return sss_filter_sanitize_ex(mem_ctx, input, sanitized, NULL);
}


Fixed code:
    -version-info 2:0:2
                           bool sanitize,
    char *exp_sanitized = NULL;
        /* cert blob is already sanitized */
        sanitize = false;
    if (sanitize) {
        ret = sss_filter_sanitize(ctx, exp, &exp_sanitized);
        if (ret != EOK) {
            CM_DEBUG(ctx, "Failed to sanitize expanded template.");
            goto done;
        }
        talloc_free(exp);
        exp = exp_sanitized;
    }

                      struct sss_cert_content *cert_content, bool sanitize,
                                  sanitize, &expanded);
static int expand_mapping_rule_ex(struct sss_certmap_ctx *ctx,
                                  bool sanitize,
        ret = get_filter(ctx, ctx->default_mapping_rule, cert_content, sanitize,
                         &filter);
                                 sanitize, &filter);
int sss_certmap_get_search_filter(struct sss_certmap_ctx *ctx,
                                  const uint8_t *der_cert, size_t der_size,
                                  char **_filter, char ***_domains)
{
    return expand_mapping_rule_ex(ctx, der_cert, der_size, true,
                                  _filter, _domains);
}

int sss_certmap_expand_mapping_rule(struct sss_certmap_ctx *ctx,
                                    const uint8_t *der_cert, size_t der_size,
                                    char **_expanded, char ***_domains)
{
    return expand_mapping_rule_ex(ctx, der_cert, der_size, false,
                                  _expanded, _domains);
}


SSS_CERTMAP_0.2 {
    global:
        sss_certmap_expand_mapping_rule;
} SSS_CERTMAP_0.1;
 * @param[in] der_cert binary blob with the DER encoded certificate
 * @param[in] der_cert binary blob with the DER encoded certificate
 * @param[out] filter  LDAP filter string, expanded templates are sanitized,
 *                     caller should free the data by calling
 *                     sss_certmap_free_filter_and_domains
/**
 * @brief Expand the mapping rule by replacing the templates
 *
 * @param[in] ctx        certmap context previously initialized with
 *                       @ref sss_certmap_init
 * @param[in] der_cert   binary blob with the DER encoded certificate
 * @param[in] der_size   size of the certificate blob
 * @param[out] expanded  expanded mapping rule, templates are filled in
 *                       verbatim in contrast to sss_certmap_get_search_filter,
 *                       caller should free the data by
 *                       calling sss_certmap_free_filter_and_domains
 * @param[out] domains   NULL-terminated array of strings with the domains the
 *                       rule applies, caller should free the data by calling
 *                       sss_certmap_free_filter_and_domains
 *
 * @return
 *  - 0:      certificate matches a rule
 *  - ENOENT: certificate does not match
 *  - EINVAL: internal error
 */
int sss_certmap_expand_mapping_rule(struct sss_certmap_ctx *ctx,
                                    const uint8_t *der_cert, size_t der_size,
                                    char **_expanded, char ***_domains);
 *        and @ref sss_certmap_expand_mapping_rule
 * @param[in]  der_cert   binary blob with the DER encoded certificate
    ret = sss_certmap_expand_mapping_rule(ctx, der, der_size,
                                          &filter, &domains);
        DEBUG(SSSDBG_OP_FAILURE, "sss_certmap_expand_mapping_rule failed.\n");
    assert_string_equal(filter, "rule100=<I>CN=Certificate\\20Authority,O=IPA.DEVEL"
                                "<S>CN=ipa-devel.ipa.devel,O=IPA.DEVEL");
    assert_null(domains);

    ret = sss_certmap_expand_mapping_rule(ctx, discard_const(test_cert_der),
                                          sizeof(test_cert_der),
                                          &filter, &domains);
    assert_int_equal(ret, 0);
    assert_non_null(filter);
    assert_string_equal(filter, "rule99=<I>CN=Certificate\\20Authority,O=IPA.DEVEL"
                                "<S>CN=ipa-devel.ipa.devel,O=IPA.DEVEL");
    assert_non_null(domains);
    assert_string_equal(domains[0], "test.dom");
    assert_null(domains[1]);

    ret = sss_certmap_expand_mapping_rule(ctx, discard_const(test_cert_der),
                                          sizeof(test_cert_der),
                                          &filter, &domains);
    assert_int_equal(ret, 0);
    assert_non_null(filter);
    ret = sss_certmap_expand_mapping_rule(ctx, discard_const(test_cert_der),
                                          sizeof(test_cert_der),
                                          &filter, &domains);
    assert_int_equal(ret, 0);
    assert_non_null(filter);
    assert_string_equal(filter, "rule98=userCertificate;binary=" TEST_CERT_BIN);
    assert_non_null(domains);
    assert_string_equal(domains[0], "test.dom");
    assert_null(domains[1]);

    assert_string_equal(filter, "rule97=<I>O=IPA.DEVEL,CN=Certificate\\20Authority"
                                "<S>CN=ipa-devel.ipa.devel,O=IPA.DEVEL");
    assert_non_null(domains);
    assert_string_equal(domains[0], "test.dom");
    assert_null(domains[1]);

    ret = sss_certmap_expand_mapping_rule(ctx, discard_const(test_cert_der),
                                          sizeof(test_cert_der),
                                          &filter, &domains);
    assert_int_equal(ret, 0);
    assert_non_null(filter);
    assert_string_equal(filter, "rule96=<I>O=IPA.DEVEL,CN=Certificate\\20Authority"
                                "<S>O=IPA.DEVEL,CN=ipa-devel.ipa.devel");
    assert_non_null(domains);
    assert_string_equal(domains[0], "test.dom");
    assert_null(domains[1]);

    ret = sss_certmap_expand_mapping_rule(ctx, discard_const(test_cert_der),
                                          sizeof(test_cert_der),
                                          &filter, &domains);
    assert_int_equal(ret, 0);
    assert_non_null(filter);
    ret = sss_certmap_expand_mapping_rule(ctx, discard_const(test_cert_der),
                                          sizeof(test_cert_der),
                                          &filter, &domains);
    assert_int_equal(ret, 0);
    assert_non_null(filter);
    assert_string_equal(filter, "(userCertificate;binary=" TEST_CERT_BIN ")");
    assert_null(domains);

    assert_string_equal(filter, "rule94=<I>O=IPA.DEVEL,CN=Certificate\\20Authority"
    ret = sss_certmap_expand_mapping_rule(ctx, discard_const(test_cert_der),
                                          sizeof(test_cert_der),
                                          &filter, &domains);
    assert_int_equal(ret, 0);
    assert_non_null(filter);
    assert_string_equal(filter, "rule94=<I>O=IPA.DEVEL,CN=Certificate Authority"
                                "<S>O=IPA.DEVEL,CN=ipa-devel.ipa.devel");
    assert_non_null(domains);
    assert_string_equal(domains[0], "test.dom");
    assert_null(domains[1]);
    ret = sss_certmap_expand_mapping_rule(ctx, discard_const(test_cert2_der),
                                          sizeof(test_cert2_der),
                                          &filter, &domains);
    assert_int_equal(ret, 0);
    assert_non_null(filter);
    assert_string_equal(filter, "(rule89=tu1@ad.devel)");
    assert_null(domains);

    assert_string_equal(filter, "rule87=<I>DC=devel,DC=ad,CN=ad-AD-SERVER-CA"
                  "<S>DC=devel,DC=ad,CN=Users,CN=t\\20u,E=test.user@email.domain");
    assert_null(domains);

    ret = sss_certmap_expand_mapping_rule(ctx, discard_const(test_cert2_der),
                                          sizeof(test_cert2_der),
                                          &filter, &domains);
    assert_int_equal(ret, 0);
    assert_non_null(filter);
    assert_string_equal(filter, "rule86=<I>DC=devel,DC=ad,CN=ad-AD-SERVER-CA"
                  "<S>DC=devel,DC=ad,CN=Users,CN=t\\20u,E=test.user@email.domain");
    assert_null(domains);

    ret = sss_certmap_expand_mapping_rule(ctx, discard_const(test_cert2_der),
                                          sizeof(test_cert2_der),
                                          &filter, &domains);
    assert_int_equal(ret, 0);
    assert_non_null(filter);
#ifndef HAVE_ERRNO_T
#define HAVE_ERRNO_T
typedef int errno_t;
#endif


errno_t sss_filter_sanitize_ex(TALLOC_CTX *mem_ctx,
                               const char *input,
                               char **sanitized,
                               const char *ignore)
{
    char *output;
    size_t i = 0;
    size_t j = 0;
    char *allowed;

    /* Assume the worst-case. We'll resize it later, once */
    output = talloc_array(mem_ctx, char, strlen(input) * 3 + 1);
    if (!output) {
        return ENOMEM;
    }

    while (input[i]) {
        /* Even though this character might have a special meaning, if it's
         * explicitly allowed, just copy it and move on
         */
        if (ignore == NULL) {
            allowed = NULL;
        } else {
            allowed = strchr(ignore, input[i]);
        }
        if (allowed) {
            output[j++] = input[i++];
            continue;
        }

        switch(input[i]) {
        case '\t':
            output[j++] = '\\';
            output[j++] = '0';
            output[j++] = '9';
            break;
        case ' ':
            output[j++] = '\\';
            output[j++] = '2';
            output[j++] = '0';
            break;
        case '*':
            output[j++] = '\\';
            output[j++] = '2';
            output[j++] = 'a';
            break;
        case '(':
            output[j++] = '\\';
            output[j++] = '2';
            output[j++] = '8';
            break;
        case ')':
            output[j++] = '\\';
            output[j++] = '2';
            output[j++] = '9';
            break;
        case '\\':
            output[j++] = '\\';
            output[j++] = '5';
            output[j++] = 'c';
            break;
        case '\r':
            output[j++] = '\\';
            output[j++] = '0';
            output[j++] = 'd';
            break;
        case '\n':
            output[j++] = '\\';
            output[j++] = '0';
            output[j++] = 'a';
            break;
        default:
            output[j++] = input[i];
        }

        i++;
    }
    output[j] = '\0';
    *sanitized = talloc_realloc(mem_ctx, output, char, j+1);
    if (!*sanitized) {
        talloc_free(output);
        return ENOMEM;
    }

    return EOK;
}

errno_t sss_filter_sanitize(TALLOC_CTX *mem_ctx,
                            const char *input,
                            char **sanitized)
{
    return sss_filter_sanitize_ex(mem_ctx, input, sanitized, NULL);
}