Commit message: mod_session: account for the '&' in identity_concat().

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1887052 13f79535-47bb-0310-9956-ffa450edef68
CVE: CVE-2021-26691
CWE: CWE-787
Vulnerability description: In Apache HTTP Server versions 2.4.0 to 2.4.46 a specially crafted SessionHeader sent by an origin server could cause a heap overflow

Project: httpd
File: mod_session.c

static apr_status_t session_identity_decode(request_rec * r, session_rec * z)
{

    char *last = NULL;
    char *encoded, *pair;
    const char *sep = "&";

    /* sanity check - anything to decode? */
    if (!z->encoded) {
        return OK;
    }

    /* decode what we have */
    encoded = apr_pstrdup(r->pool, z->encoded);
    pair = apr_strtok(encoded, sep, &last);
    while (pair && pair[0]) {
        char *plast = NULL;
        const char *psep = "=";
        char *key = apr_strtok(pair, psep, &plast);
        if (key && *key) {
            char *val = apr_strtok(NULL, sep, &plast);
            if (!val || !*val) {
                apr_table_unset(z->entries, key);
            }
            else if (!ap_unescape_urlencoded(key) && !ap_unescape_urlencoded(val)) {
                if (!strcmp(SESSION_EXPIRY, key)) {
                    z->expiry = (apr_time_t) apr_atoi64(val);
                }
                else {
                    apr_table_set(z->entries, key, val);
                }
            }
        }
        pair = apr_strtok(NULL, sep, &last);
    }
    z->encoded = NULL;
    return OK;

}

Project: httpd
File: mod_session.c

     set_session_env(cmd_parms * parms, void *dconf, int flag)
{
    session_dir_conf *conf = dconf;

    conf->env = flag;
    conf->env_set = 1;

    return NULL;
}

Project: httpd
File: mod_session.c

static apr_status_t ap_session_set(request_rec * r, session_rec * z,
        const char *key, const char *value)
{
    if (!z) {
        apr_status_t rv;
        rv = ap_session_load(r, &z);
        if (APR_SUCCESS != rv) {
            return rv;
        }
    }
    if (z) {
        if (value) {
            apr_table_set(z->entries, key, value);
        }
        else {
            apr_table_unset(z->entries, key);
        }
        z->dirty = 1;
    }
    return APR_SUCCESS;
}

Project: httpd
File: mod_session.c

static apr_status_t ap_session_get(request_rec * r, session_rec * z,
        const char *key, const char **value)
{
    if (!z) {
        apr_status_t rv;
        rv = ap_session_load(r, &z);
        if (APR_SUCCESS != rv) {
            return rv;
        }
    }
    if (z && z->entries) {
        *value = apr_table_get(z->entries, key);
    }

    return OK;
}

Project: httpd
File: mod_session.c

static int identity_concat(void *v, const char *key, const char *val)
{
    char *slider = v;
    int length = strlen(slider);
    slider += length;
    if (length) {
        *slider = '&';
        slider++;
    }
    ap_escape_urlencoded_buffer(slider, key);
    slider += strlen(slider);
    *slider = '=';
    slider++;
    ap_escape_urlencoded_buffer(slider, val);
    return 1;
}

Project: httpd
File: mod_session.c

static apr_status_t session_identity_encode(request_rec * r, session_rec * z)
{
    char *buffer = NULL;
    int length = 0;
    if (z->expiry) {
        char *expiry = apr_psprintf(z->pool, "%" APR_INT64_T_FMT, z->expiry);
        apr_table_setn(z->entries, SESSION_EXPIRY, expiry);
    }
    apr_table_do(identity_count, &length, z->entries, NULL);
    buffer = apr_pcalloc(r->pool, length + 1);
    apr_table_do(identity_concat, buffer, z->entries, NULL);
    z->encoded = buffer;
    return OK;

}

Project: httpd
File: mod_session.c

static const char *add_session_include(cmd_parms * cmd, void *dconf, const char *f)
{
    session_dir_conf *conf = dconf;

    const char **new = apr_array_push(conf->includes);
    *new = f;

    return NULL;
}

Project: httpd
File: mod_session.c

static void *create_session_dir_config(apr_pool_t * p, char *dummy)
{
    session_dir_conf *new =
    (session_dir_conf *) apr_pcalloc(p, sizeof(session_dir_conf));

    new->includes = apr_array_make(p, 10, sizeof(const char **));
    new->excludes = apr_array_make(p, 10, sizeof(const char **));

    return (void *) new;
}

Project: httpd
File: mod_session.c

static int session_fixups(request_rec * r)
{
    session_dir_conf *conf = ap_get_module_config(r->per_dir_config,
                                                  &session_module);

    session_rec *z = NULL;

    /* if an error occurs or no session has been configured, we ignore
     * the broken session and allow it to be recreated from scratch on save
     * if necessary.
     */
    ap_session_load(r, &z);

    if (conf->env) {
        if (z) {
            session_identity_encode(r, z);
            if (z->encoded) {
                apr_table_set(r->subprocess_env, HTTP_SESSION, z->encoded);
                z->encoded = NULL;
            }
        }
        apr_table_unset(r->headers_in, "Session");
    }

    return OK;

}

Project: httpd
File: mod_session.c

static apr_status_t ap_session_save(request_rec * r, session_rec * z)
{
    if (z) {
        apr_time_t now = apr_time_now();
        apr_time_t initialExpiry = z->expiry;
        int rv = 0;

        session_dir_conf *dconf = ap_get_module_config(r->per_dir_config,
                                                       &session_module);

        /* sanity checks, should we try save at all? */
        if (z->written) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(01818)
                          "attempt made to save the session twice, "
                          "session not saved: %s", r->uri);
            return APR_EGENERAL;
        }
        if (z->expiry && z->expiry < now) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01819)
                          "attempt made to save a session when the session had already expired, "
                          "session not saved: %s", r->uri);
            return APR_EGENERAL;
        }

        /* reset the expiry back to maxage, if the expiry is present */
        if (dconf->maxage) {
            z->expiry = now + dconf->maxage * APR_USEC_PER_SEC;
            z->maxage = dconf->maxage;
        }

        /* reset the expiry before saving if present */
        if (z->dirty && z->maxage) {
            z->expiry = now + z->maxage * APR_USEC_PER_SEC;
        } 

        /* don't save if the only change is the expiry by a small amount */
        if (!z->dirty && dconf->expiry_update_time
                && (z->expiry - initialExpiry < dconf->expiry_update_time)) {
            return APR_SUCCESS;
        }

        /* also don't save sessions that didn't change at all */
        if (!z->dirty && !z->maxage) {
            return APR_SUCCESS;
        }

        /* encode the session */
        rv = ap_run_session_encode(r, z);
        if (OK != rv) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01820)
                          "error while encoding the session, "
                          "session not saved: %s", r->uri);
            return rv;
        }

        /* try the save */
        rv = ap_run_session_save(r, z);
        if (DECLINED == rv) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01821)
                          "session is enabled but no session modules have been configured, "
                          "session not saved: %s", r->uri);
            return APR_EGENERAL;
        }
        else if (OK != rv) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01822)
                          "error while saving the session, "
                          "session not saved: %s", r->uri);
            return rv;
        }
        else {
            z->written = 1;
        }
    }

    return APR_SUCCESS;

}

Project: httpd
File: mod_session.c

     set_session_maxage(cmd_parms * parms, void *dconf, const char *arg)
{
    session_dir_conf *conf = dconf;

    conf->maxage = atol(arg);
    conf->maxage_set = 1;

    return NULL;
}

Project: httpd
File: mod_session.c

static void session_insert_output_filter(request_rec * r)
{
    ap_add_output_filter("MOD_SESSION_OUT", NULL, r, r->connection);
}

Project: httpd
File: mod_session.c

static const char *add_session_exclude(cmd_parms * cmd, void *dconf, const char *f)
{
    session_dir_conf *conf = dconf;

    const char **new = apr_array_push(conf->excludes);
    *new = f;

    return NULL;
}

Project: httpd
File: mod_session.c

static void *merge_session_dir_config(apr_pool_t * p, void *basev, void *addv)
{
    session_dir_conf *new = (session_dir_conf *) apr_pcalloc(p, sizeof(session_dir_conf));
    session_dir_conf *add = (session_dir_conf *) addv;
    session_dir_conf *base = (session_dir_conf *) basev;

    new->enabled = (add->enabled_set == 0) ? base->enabled : add->enabled;
    new->enabled_set = add->enabled_set || base->enabled_set;
    new->maxage = (add->maxage_set == 0) ? base->maxage : add->maxage;
    new->maxage_set = add->maxage_set || base->maxage_set;
    new->header = (add->header_set == 0) ? base->header : add->header;
    new->header_set = add->header_set || base->header_set;
    new->env = (add->env_set == 0) ? base->env : add->env;
    new->env_set = add->env_set || base->env_set;
    new->includes = apr_array_append(p, base->includes, add->includes);
    new->excludes = apr_array_append(p, base->excludes, add->excludes);
    new->expiry_update_time = (add->expiry_update_set == 0)
                                ? base->expiry_update_time
                                : add->expiry_update_time;
    new->expiry_update_set = add->expiry_update_set || base->expiry_update_set;

    return new;
}

Project: httpd
File: mod_session.c

     set_session_header(cmd_parms * parms, void *dconf, const char *arg)
{
    session_dir_conf *conf = dconf;

    conf->header = arg;
    conf->header_set = 1;

    return NULL;
}

Project: httpd
File: mod_session.c

     set_session_expiry_update(cmd_parms * parms, void *dconf, const char *arg)
{
    session_dir_conf *conf = dconf;

    conf->expiry_update_time = atoi(arg);
    if (conf->expiry_update_time < 0) {
        return "SessionExpiryUpdateInterval must be zero (disable) or a positive value";
    }
    conf->expiry_update_time = apr_time_from_sec(conf->expiry_update_time);
    conf->expiry_update_set = 1;

    return NULL;
}

Project: httpd
File: mod_session.c

static int session_included(request_rec * r, session_dir_conf * conf)
{

    const char **includes = (const char **) conf->includes->elts;
    const char **excludes = (const char **) conf->excludes->elts;
    int included = 1;                /* defaults to included */
    int i;

    if (conf->includes->nelts) {
        included = 0;
        for (i = 0; !included && i < conf->includes->nelts; i++) {
            const char *include = includes[i];
            if (strncmp(r->uri, include, strlen(include)) == 0) {
                included = 1;
            }
        }
    }

    if (conf->excludes->nelts) {
        for (i = 0; included && i < conf->excludes->nelts; i++) {
            const char *exclude = excludes[i];
            if (strncmp(r->uri, exclude, strlen(exclude)) == 0) {
                included = 0;
            }
        }
    }

    return included;
}

Project: httpd
File: mod_session.c

static void register_hooks(apr_pool_t * p)
{
    ap_register_output_filter("MOD_SESSION_OUT", session_output_filter,
                              NULL, AP_FTYPE_CONTENT_SET);
    ap_hook_insert_filter(session_insert_output_filter, NULL, NULL,
                          APR_HOOK_MIDDLE);
    ap_hook_insert_error_filter(session_insert_output_filter,
                                NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_fixups(session_fixups, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_session_encode(session_identity_encode, NULL, NULL,
                           APR_HOOK_REALLY_FIRST);
    ap_hook_session_decode(session_identity_decode, NULL, NULL,
                           APR_HOOK_REALLY_LAST);
    APR_REGISTER_OPTIONAL_FN(ap_session_get);
    APR_REGISTER_OPTIONAL_FN(ap_session_set);
    APR_REGISTER_OPTIONAL_FN(ap_session_load);
    APR_REGISTER_OPTIONAL_FN(ap_session_save);
}

Project: httpd
File: mod_session.c

static apr_status_t ap_session_load(request_rec * r, session_rec ** z)
{

    session_dir_conf *dconf = ap_get_module_config(r->per_dir_config,
                                                   &session_module);
    apr_time_t now;
    session_rec *zz = NULL;
    int rv = 0;

    /* is the session enabled? */
    if (!dconf || !dconf->enabled) {
        return APR_SUCCESS;
    }

    /* should the session be loaded at all? */
    if (!session_included(r, dconf)) {
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(01814)
                      "excluded by configuration for: %s", r->uri);
        return APR_SUCCESS;
    }

    /* load the session from the session hook */
    rv = ap_run_session_load(r, &zz);
    if (DECLINED == rv) {
        ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(01815)
                      "session is enabled but no session modules have been configured, "
                      "session not loaded: %s", r->uri);
        return APR_EGENERAL;
    }
    else if (OK != rv) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01816)
                      "error while loading the session, "
                      "session not loaded: %s", r->uri);
        return rv;
    }

    /* found a session that hasn't expired? */
    now = apr_time_now();

    if (zz) {
        /* load the session attributes */
        rv = ap_run_session_decode(r, zz);
 
        /* having a session we cannot decode is just as good as having
           none at all */
       if (OK != rv) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(01817)
                    "error while decoding the session, "
                    "session not loaded: %s", r->uri);
            /* preserve pointers to zz in load/save providers */
            memset(zz, 0, sizeof(session_rec));
            zz->pool = r->pool;
            zz->entries = apr_table_make(zz->pool, 10);
        }

       /* invalidate session if session is expired */
        if (zz && zz->expiry && zz->expiry < now) {
            ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, "session is expired");
            /* preserve pointers to zz in load/save providers */
            memset(zz, 0, sizeof(session_rec));
            zz->pool = r->pool;
            zz->entries = apr_table_make(zz->pool, 10);
        }
    }

    /* no luck, create a blank session. Note that the included session_load 
     * providers will return new sessions during session_load when configured.
     */
    if (!zz) {
        zz = (session_rec *) apr_pcalloc(r->pool, sizeof(session_rec));
        zz->pool = r->pool;
        zz->entries = apr_table_make(zz->pool, 10);
    }

    /* make sure the expiry and maxage are set, if present */
    if (dconf->maxage) {
        if (!zz->expiry) {
            zz->expiry = now + dconf->maxage * APR_USEC_PER_SEC;
        }
        zz->maxage = dconf->maxage;
    }

    *z = zz;

    return APR_SUCCESS;

}

Project: httpd
File: mod_session.c

     set_session_enable(cmd_parms * parms, void *dconf, int flag)
{
    session_dir_conf *conf = dconf;

    conf->enabled = flag;
    conf->enabled_set = 1;

    return NULL;
}

Project: httpd
File: mod_session.c

static int identity_count(void *v, const char *key, const char *val)
{
    int *count = v;
    *count += strlen(key) * 3 + strlen(val) * 3 + 2;
    return 1;
}

Project: httpd
File: mod_session.c

static apr_status_t session_output_filter(ap_filter_t * f,
        apr_bucket_brigade * in)
{

    /* save all the sessions in all the requests */
    request_rec *r = f->r->main;
    if (!r) {
        r = f->r;
    }
    while (r) {
        session_rec *z = NULL;
        session_dir_conf *conf = ap_get_module_config(r->per_dir_config,
                                                      &session_module);

        /* load the session, or create one if necessary */
        /* when unset or on error, z will be NULL */
        ap_session_load(r, &z);
        if (!z || z->written) {
            r = r->next;
            continue;
        }

        /* if a header was specified, insert the new values from the header */
        if (conf->header_set) {
            const char *override = apr_table_get(r->err_headers_out, conf->header);
            if (!override) {
                override = apr_table_get(r->headers_out, conf->header);
            }
            if (override) {
                apr_table_unset(r->err_headers_out, conf->header);
                apr_table_unset(r->headers_out, conf->header);
                z->encoded = override;
                z->dirty = 1;
                session_identity_decode(r, z);
            }
        }

        /* save away the session, and we're done */
        /* when unset or on error, we've complained to the log */
        ap_session_save(r, z);

        r = r->next;
    }

    /* remove ourselves from the filter chain */
    ap_remove_output_filter(f);

    /* send the data up the stack */
    return ap_pass_brigade(f->next, in);

}