Commit b005afb0 authored by William A. Rowe Jr's avatar William A. Rowe Jr
Browse files

  Revamped ap_directory_walk logic, without a path_info helper is now
  activated.  It may be bumpy for a few days, and we have more optimizations
  to put in place, but it's time to get this in the developer's test code.


git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@91359 13f79535-47bb-0310-9956-ffa450edef68
parent 455e41fa
Loading
Loading
Loading
Loading
+1 −455
Original line number Diff line number Diff line
@@ -415,455 +415,6 @@ static int resolve_symlink(char *d, apr_finfo_t *lfi, int opts, apr_pool_t *p)
    return OK;
}

/* #define REPLACE_PATH_INFO_METHOD
 */
#ifndef REPLACE_PATH_INFO_METHOD

static int check_symlinks(char *d, int opts, apr_pool_t *p)
{
#if defined(OS2)
    /* OS/2 doesn't have symlinks */
    return OK;
#else
    apr_finfo_t lfi, fi;
    char *lastp;
    int res;

    if (opts & OPT_SYM_LINKS)
        return OK;

    /*
     * Strip trailing '/', if any, off what we're checking; trailing slashes
     * make some systems follow symlinks to directories even in lstat().
     * After we've done the lstat, put it back.  Also, don't bother checking
     * '/' at all...
     * 
     * Note that we don't have to worry about multiple slashes here because of
     * no2slash() below...
     */

    lastp = d + strlen(d) - 1;
    if (lastp == d)
        return OK;              /* Root directory, '/' */

    if (*lastp == '/')
        *lastp = '\0';
    else
        lastp = NULL;

    res = apr_lstat(&lfi, d, APR_FINFO_TYPE | APR_FINFO_OWNER, p);

    if (lastp)
        *lastp = '/';

    /*
     * Note that we don't reject accesses to nonexistent files (multiviews or
     * the like may cons up a way to run the transaction anyway)...
     */

    if ((res != APR_SUCCESS && res != APR_INCOMPLETE)
           || (lfi.filetype != APR_LNK))
        return OK;

    /* OK, it's a symlink.  May still be OK with OPT_SYM_OWNER */

    if (!(opts & OPT_SYM_OWNER))
        return HTTP_FORBIDDEN;

    /* OPT_SYM_OWNER only works if we can get the owner from the file */

    if (res != APR_SUCCESS)
        return HTTP_FORBIDDEN;

    if (apr_stat(&fi, d, APR_FINFO_OWNER, p) != APR_SUCCESS)
        return HTTP_FORBIDDEN;

    /* TODO: replace with an apr_compare_users() fn */
    return (fi.user == lfi.user) ? OK : HTTP_FORBIDDEN;

#endif
}


/* Dealing with the file system to get PATH_INFO
 */
static int get_path_info(request_rec *r)
{
    char *cp;
    char *path = r->filename;
    char *end = &path[strlen(path)];
    char *last_cp = NULL;
    int rv;
#if defined(HAVE_DRIVE_LETTERS) || defined(HAVE_UNC_PATHS)
    char bStripSlash=1;
#endif

    if (r->finfo.filetype != APR_NOFILE) {
	/* assume path_info already set */
	return OK;
    }

#ifdef HAVE_DRIVE_LETTERS
    /* If the directory is x:\, then we don't want to strip
     * the trailing slash since x: is not a valid directory.
     */
    if (strlen(path) == 3 && path[1] == ':' && path[2] == '/')
        bStripSlash = 0;
#endif

#ifdef HAVE_UNC_PATHS
    /* If UNC name == //machine/share/, do not 
     * advance over the trailing slash.  Any other
     * UNC name is OK to strip the slash.
     */
    cp = end;
    if (path[0] == '/' && path[1] == '/' && 
        path[2] != '/' && cp[-1] == '/') {
        char *p;
        int iCount=0;
        p = path;
        while ((p = strchr(p,'/')) != NULL) {
            p++;
            iCount++;
        }
    
        if (iCount == 4)
            bStripSlash = 0;
    }
#endif
   
#if defined(HAVE_DRIVE_LETTERS) || defined(HAVE_UNC_PATHS)
    if (bStripSlash)
#endif
        /* Advance over trailing slashes ... NOT part of filename 
         * if file is not a UNC name (Win32 only).
         */
        for (cp = end; cp > path && cp[-1] == '/'; --cp)
            continue;

    while (cp > path) {

        /* See if the pathname ending here exists... */
        *cp = '\0';

        /* ### We no longer need the test ap_os_is_filename_valid() here 
         * since apr_stat isn't a posix thing - it's apr_stat's responsibility
         * to handle whatever path string arrives at its door - by platform
         * and volume restrictions as applicable... 
         * TODO: This code becomes even simpler if apr_stat grows 
         * an APR_PATHINCOMPLETE result to indicate that we are staring at
         * an partial virtual root.  Only OS2/Win32/Netware need apply it :-)
         */
        rv = apr_stat(&r->finfo, path, APR_FINFO_MIN, r->pool);

        if (cp != end)
            *cp = '/';

        if (rv == APR_SUCCESS || rv == APR_INCOMPLETE) {
            /*
             * Aha!  Found something.  If it was a directory, we will search
             * contents of that directory for a multi_match, so the PATH_INFO
             * argument starts with the component after that.
             */
            if (r->finfo.filetype == APR_DIR && last_cp) {
                r->finfo.filetype = APR_NOFILE;  /* No such file... */
                cp = last_cp;
            }

            r->path_info = apr_pstrdup(r->pool, cp);
            *cp = '\0';
            return OK;
        }
        
        if (APR_STATUS_IS_ENOENT(rv) || APR_STATUS_IS_ENOTDIR(rv)) {
            last_cp = cp;

            while (--cp > path && *cp != '/')
                continue;

            while (cp > path && cp[-1] == '/')
                --cp;
        }
        else {
            if (APR_STATUS_IS_EACCES(rv))
                ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                              "access to %s denied", r->uri);
            else
                ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                              "access to %s failed", r->uri);
            return HTTP_FORBIDDEN;
        }
    }
    return OK;
}

AP_DECLARE(int) ap_directory_walk(request_rec *r)
{
    core_server_config *sconf = ap_get_module_config(r->server->module_config,
                                                     &core_module);
    ap_conf_vector_t *per_dir_defaults = r->server->lookup_defaults;
    ap_conf_vector_t **sec_dir = (ap_conf_vector_t **) sconf->sec_dir->elts;
    int num_sec = sconf->sec_dir->nelts;
    char *test_filename;
    char *test_dirname;
    int res;
    unsigned i, num_dirs;
    int j, test_filename_len;
    unsigned iStart = 1;
    ap_conf_vector_t *entry_config;
    ap_conf_vector_t *this_conf;
    core_dir_config *entry_core;

    /* "OK" as a response to a real problem is not _OK_, but to allow broken 
     * modules to proceed, we will permit the not-a-path filename to pass here.
     * We must catch it later if it's heading for the core handler.  Leave an 
     * INFO note here for module debugging.
     */
    if (r->filename == NULL || !ap_os_is_path_absolute(r->pool, r->filename)) {
        ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_INFO, 0, r,
                      "Module bug?  Request filename path %s is missing or "
                      "or not absolute for uri %s", 
                      r->filename ? r->filename : "<NULL>", r->uri);
        return OK;
    }

    /*
     * Go down the directory hierarchy.  Where we have to check for symlinks,
     * do so.  Where a .htaccess file has permission to override anything,
     * try to find one.  If either of these things fails, we could poke
     * around, see why, and adjust the lookup_rec accordingly --- this might
     * save us a call to get_path_info (with the attendant stat()s); however,
     * for the moment, that's not worth the trouble.
     */
    res = get_path_info(r);
    if (res != OK) {
        return res;
    }

    /* XXX Momentary period of extreme danger, Will Robinson.
     * Removed ap_os_canonical_filename.  Anybody munging the
     * r->filename better have pre-canonicalized the name that
     * they just changed.  Since the two most key functions
     * in the entire server, ap_server_root_relative() and
     * ap_make_full_path now canonicalize as they go.
     *
     * To be very safe, the server is in hyper-paranoid mode.
     * That means that non-canonical paths will be captured and
     * denied.  This is very cpu/fs intensive, we need to finish
     * auditing, and remove the paranoia trigger.
     */
    if (r->filename == r->canonical_filename)
#ifdef NO_LONGER_PARANOID
        test_filename = apr_pstrdup(r->pool, r->filename);
#else
    {
        if (apr_filepath_merge(&test_filename, "", r->filename,
                               APR_FILEPATH_NOTRELATIVE | APR_FILEPATH_TRUENAME,
                               r->pool) != APR_SUCCESS
               || strcmp(test_filename, r->filename) != 0) {
            ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r,
                          "Module bug?  Filepath: %s is not the canonical %s", 
                          r->filename, test_filename);
            return HTTP_FORBIDDEN;
        }
    }
#endif
    else {
        /* Apparently, somebody didn't know to update r->canonical_filename
         * which is lucky, since they didn't canonicalize r->filename either.
         */
        if (apr_filepath_merge(&test_filename, NULL, r->filename,
                               APR_FILEPATH_NOTRELATIVE | APR_FILEPATH_TRUENAME,
                               r->pool) != APR_SUCCESS) {
            ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r,
                          "Module bug?  Filepath: %s is not an absolute path", 
                          r->filename);
            return HTTP_FORBIDDEN;
        }
        if (strcmp(r->filename, test_filename) != 0)
            r->filename = apr_pstrdup(r->pool, test_filename);
        r->canonical_filename = r->filename;
    }

    num_dirs = ap_count_dirs(test_filename);

    if ((res = check_safe_file(r))) {
        return res;
    }

    test_filename_len = strlen(test_filename);
    if (test_filename[test_filename_len - 1] == '/')
        --num_dirs;

    if (r->finfo.filetype == APR_DIR)
        ++num_dirs;

    /*
     * We will use test_dirname as scratch space while we build directory
     * names during the walk.  Profiling shows directory_walk to be a busy
     * function so we try to avoid allocating lots of extra memory here.
     * We need 2 extra bytes, one for trailing \0 and one because
     * make_dirstr_prefix will add potentially one extra /.
     */
    test_dirname = apr_palloc(r->pool, test_filename_len + 2);

    /* XXX The garbage below disappears in the new directory_walk;
     */

#if defined(HAVE_UNC_PATHS)
    /* If the name is a UNC name, then do not perform any true file test
     * against the machine name (start at //machine/share/)
     * This is optimized to use the normal walk (skips the redundant '/' root)
     */
    if (num_dirs > 3 && test_filename[0] == '/' && test_filename[1] == '/')
        iStart = 4;
#endif

#if defined(NETWARE)
    /* If the name is a fully qualified volume name, then do not perform any
     * true file test on the machine name (start at machine/share:/)
     * XXX: The implementation eludes me at this moment... 
     *      Does this make sense?  Please test!
     */
    if (num_dirs > 1 && strchr(test_filename, '/') < strchr(test_filename, ':'))
        iStart = 2;
#endif

    /* i keeps track of how many segments we are testing
     * j keeps track of which section we're on, see core_reorder_directories 
     */
    j = 0;
    for (i = 1; i <= num_dirs; ++i) {
        int overrides_here;
        core_dir_config *core_dir = ap_get_module_config(per_dir_defaults,
                                                         &core_module);

        /*
         * XXX: this could be made faster by only copying the next component
         * rather than copying the entire thing all over.
         */
        ap_make_dirstr_prefix(test_dirname, test_filename, i);

        /*
         * Do symlink checks first, because they are done with the
         * permissions appropriate to the *parent* directory...
         */

        /* Test only real names (after the root) against the real filesystem */
        if ((i > iStart) && (res = check_symlinks(test_dirname, core_dir->opts, r->pool))) {
            ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r,
                        "Symbolic link not allowed: %s", test_dirname);
            return res;
        }

        /*
         * Begin *this* level by looking for matching <Directory> sections
         * from access.conf.
         */

        for (; j < num_sec; ++j) {
            char *entry_dir;

            entry_config = sec_dir[j];
            entry_core = ap_get_module_config(entry_config, &core_module);
            entry_dir = entry_core->d;

            if (entry_core->r || entry_core->d_components > i)
                break;

            this_conf = NULL;
            /* We will always add in '0' element components, e.g. plain old
             * <Directory >, and <Directory "/"> is classified as zero 
             * so that Win32/Netware/OS2 etc all pick that up.
             */
            if (!entry_core->d_components) {
                this_conf = entry_config;
            }
            else if (entry_core->d_is_fnmatch) {
                if (!apr_fnmatch(entry_dir, test_dirname, FNM_PATHNAME)) {
                    this_conf = entry_config;
                }
            }
            else if (!strcmp(test_dirname, entry_dir))
                this_conf = entry_config;

            if (this_conf) {
                per_dir_defaults = ap_merge_per_dir_configs(r->pool,
                                                            per_dir_defaults,
                                                            this_conf);
                core_dir = ap_get_module_config(per_dir_defaults,
                                                &core_module);
            }
        }
        overrides_here = core_dir->override;

        /* If .htaccess files are enabled, check for one. */

        /* Test only legal names against the real filesystem */
        if ((i >= iStart) && overrides_here) {
            ap_conf_vector_t *htaccess_conf = NULL;

            res = ap_parse_htaccess(&htaccess_conf, r, overrides_here,
                                    apr_pstrdup(r->pool, test_dirname),
                                    sconf->access_name);
            if (res)
                return res;

            if (htaccess_conf) {
                per_dir_defaults = ap_merge_per_dir_configs(r->pool,
							    per_dir_defaults,
							    htaccess_conf);
		r->per_dir_config = per_dir_defaults;
	    }
        }
    }

    /*
     * Now we'll deal with the regexes.
     */
    for (; j < num_sec; ++j) {

        entry_config = sec_dir[j];
        entry_core = ap_get_module_config(entry_config, &core_module);

        if (!entry_core->r) {
            continue;
        }
        if (!ap_regexec(entry_core->r, test_dirname, 0, NULL, REG_NOTEOL)) {
            per_dir_defaults = ap_merge_per_dir_configs(r->pool,
                                                        per_dir_defaults,
                                                        entry_config);
        }
    }
    r->per_dir_config = per_dir_defaults;

    /*
     * Symlink permissions are determined by the parent.  If the request is
     * for a directory then applying the symlink test here would use the
     * permissions of the directory as opposed to its parent.  Consider a
     * symlink pointing to a dir with a .htaccess disallowing symlinks.  If
     * you access /symlink (or /symlink/) you would get a 403 without this
     * APR_DIR test.  But if you accessed /symlink/index.html, for example,
     * you would *not* get the 403.
     */
    if (r->finfo.filetype != APR_DIR
        && (res = check_symlinks(r->filename, ap_allow_options(r), r->pool))) {
        ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r,
                    "Symbolic link not allowed: %s", r->filename);
        return res;
    }

    /* Save a dummy userdata element till we optimize this function.
     * If this userdata is set, directory_walk has run.
     */
    apr_pool_userdata_set((void *)1, "ap_directory_walk::cache",
                          apr_pool_cleanup_null, r->pool);

    return OK;                  /* Can only "fail" if access denied by the
                                 * symlink goop. */
}

#else /* defined REPLACE_PATH_INFO_METHOD */

/*****************************************************************
 *
@@ -1372,8 +923,6 @@ minimerge2:
    return OK;
}

#endif /* defined REPLACE_PATH_INFO_METHOD */


AP_DECLARE(int) ap_location_walk(request_rec *r)
{
@@ -1820,7 +1369,6 @@ AP_DECLARE(request_rec *) ap_sub_req_lookup_dirent(const apr_finfo_t *dirent,
    
    ap_parse_uri(rnew, rnew->uri);    /* fill in parsed_uri values */

#ifdef REPLACE_PATH_INFO_METHOD
    /* Preserve the apr_stat results, and perhaps we also tag that
     * symlinks were tested and/or found for r->filename.  
     */
@@ -1850,7 +1398,6 @@ AP_DECLARE(request_rec *) ap_sub_req_lookup_dirent(const apr_finfo_t *dirent,
    else {
        memcpy (&rnew->finfo, dirent, sizeof(apr_finfo_t));
    }
#endif /* defined REPLACE_PATH_INFO_METHOD */

    if ((res = ap_process_request_internal(rnew))) {
        rnew->status = res;
@@ -1901,7 +1448,6 @@ AP_DECLARE(request_rec *) ap_sub_req_lookup_file(const char *new_file,
           && rnew->filename[fdirlen] 
           && ap_strchr_c(rnew->filename + fdirlen, '/') == NULL)
    {
#ifdef REPLACE_PATH_INFO_METHOD
        apr_status_t rv;
        if (ap_allow_options(rnew) & OPT_SYM_LINKS) {
            if (((rv = apr_stat(&rnew->finfo, rnew->filename,
@@ -1915,7 +1461,7 @@ AP_DECLARE(request_rec *) ap_sub_req_lookup_file(const char *new_file,
                                                      && (rv != APR_INCOMPLETE))
                rnew->finfo.filetype = 0;
        }
#endif /* defined REPLACE_PATH_INFO_METHOD */

        if (r->uri && *r->uri) {
            char *udir = ap_make_dirstr_parent(rnew->pool, r->uri);
            rnew->uri = ap_make_full_path(rnew->pool, udir, rnew->filename + fdirlen);