Commit message: Fix redirect URL validation bypass

It turns out that browsers silently convert backslash characters into
forward slashes, while apr_uri_parse() does not.

This mismatch allows an attacker to bypass the redirect URL validation
by using an URL like:

  https://sp.example.org/mellon/logout?ReturnTo=https:%5c%5cmalicious.example.org/

mod_auth_mellon will assume that it is a relative URL and allow the
request to pass through, while the browsers will use it as an absolute
url and redirect to https://malicious.example.org/ .

This patch fixes this issue by rejecting all redirect URLs with
backslashes.
CVE: CVE-2019-3877
CWE: CWE-601
Vulnerability description: A vulnerability was found in mod_auth_mellon before v0.14.2. An open redirect in the logout URL allows requests with backslashes to pass through by assuming that it is a relative URL, while the browsers silently convert backslash characters into forward slashes treating them as an absolute URL. This mismatch allows an attacker to bypass the redirect URL validation logic in apr_uri_parse function.

Project: mod_auth_mellon
File: auth_mellon_util.c

am_file_data_t *am_file_data_copy(apr_pool_t *pool,
                                  am_file_data_t *src_file_data)
{
    am_file_data_t *dst_file_data = NULL;

    if ((dst_file_data = am_file_data_new(pool, src_file_data->path)) == NULL) {
        return NULL;
    }

    dst_file_data->path = apr_pstrdup(pool, src_file_data->path);
    dst_file_data->stat_time = src_file_data->stat_time;
    dst_file_data->finfo = src_file_data->finfo;
    dst_file_data->contents = apr_pstrdup(pool, src_file_data->contents);
    dst_file_data->read_time = src_file_data->read_time;
    dst_file_data->rv = src_file_data->rv;
    dst_file_data->strerror = apr_pstrdup(pool, src_file_data->strerror);
    dst_file_data->generated = src_file_data->generated;

    return dst_file_data;
}

Project: mod_auth_mellon
File: auth_mellon_util.c

const char *am_get_config_langstring(apr_hash_t *h, const char *lang)
{
    char *string;

    if (lang == NULL) {
        lang = "";
    }

    string = (char *)apr_hash_get(h, lang, APR_HASH_KEY_STRING);

    return string;
}

Project: mod_auth_mellon
File: auth_mellon_util.c

const char *am_get_mime_header(request_rec *r, const char *m, const char *h) 
{
    const char *line;
    char *l1;
    const char *value;
    char *l2;

    for (line = am_xstrtok(r, m, "\n", &l1); line && *line; 
         line = am_xstrtok(r, NULL, "\n", &l1)) {

        am_strip_blank(&line);

        if (((value = am_xstrtok(r, line, ":", &l2)) != NULL) &&
            (strcasecmp(value, h) == 0)) {
            if ((value = am_xstrtok(r, NULL, ":", &l2)) != NULL)
                am_strip_blank(&value);
            return value;
        }
   }
   return NULL;
}

Project: mod_auth_mellon
File: auth_mellon_util.c

int am_postdir_cleanup(request_rec *r)
{
    am_mod_cfg_rec *mod_cfg;
    apr_dir_t *postdir;
    apr_status_t rv;
    char error_buffer[64];
    apr_finfo_t afi;
    char *fname;
    int count;
    apr_time_t expire_before;

    mod_cfg = am_get_mod_cfg(r->server);

    /* The oldes file we should keep. Delete files that are older. */
    expire_before = apr_time_now() - mod_cfg->post_ttl * APR_USEC_PER_SEC;

    /*
     * Open our POST directory or create it. 
     */
    rv = apr_dir_open(&postdir, mod_cfg->post_dir, r->pool);
    if (rv != 0) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Unable to open MellonPostDirectory \"%s\": %s",
                      mod_cfg->post_dir,
                      apr_strerror(rv, error_buffer, sizeof(error_buffer)));
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    /*
     * Purge outdated items
     */
    count = 0;
    do {
        rv = apr_dir_read(&afi, APR_FINFO_NAME|APR_FINFO_CTIME, postdir);
        if (rv != OK)
            break;

        /* Skip dot_files */
        if (afi.name[0] == '.')
             continue;

        if (afi.ctime < expire_before) {
            fname = apr_psprintf(r->pool, "%s/%s", mod_cfg->post_dir, afi.name);
            (void)apr_file_remove(fname , r->pool); 
        } else {
            count++;
        }
    } while (1 /* CONSTCOND */);

    (void)apr_dir_close(postdir);

    if (count >= mod_cfg->post_count) {
        AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r,
                      "Too many saved POST sessions. "
                      "Increase MellonPostCount directive.");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    return OK;
}

Project: mod_auth_mellon
File: auth_mellon_util.c

parse_error_msg(apr_array_header_t *tokens, apr_size_t index)
{
    if (index >= tokens->nelts) {
        return "end of string";
    }

    return apr_psprintf(tokens->pool, "\"%s\" at position %" APR_SIZE_T_FMT,
                        APR_ARRAY_IDX(tokens, index, Token).str,
                        APR_ARRAY_IDX(tokens, index, Token).offset);
}