dongtan9518
2014-11-27 17:22 浏览 44
已采纳

使用htaccess和PHP拒绝目录和文件访问

I'm currently working on a web project for a gaming group, and I'm trying to test myself and expand my general knowledge and toolbox. One of the features I'm working on requires me to "clean" the URL of a requested file. I have the system in place, and it works beautifully. Now, in order for my website to work 'flawlessly', I need to deny access to certain folders, and all files within, through htaccess. But there's a slight catch... It needs to operate without the user even realising.

My file structure, preferred access method, path, and status, are as follows:

rogue                   {allow}  {direct}   {%domain%/dashboard/}  {200}
    css                 {deny}   {direct}   {%domain%/css/}        {403}
    js                  {deny}   {direct}   {%domain%/js/}         {403}
    fonts               {deny}   {direct}   {%domain%/fonts/}      {403}
    files               {deny}   {direct}   {%domain%/files/}      {403}
    image               {deny}   {direct}   {%domain%/image/}      {403}
    includes            {deny}   {direct}   {%domain%/includes/}   {403}
    php                 {deny}   {direct}   {%domain%/php/}        {403}
    index.php           {allow}  {direct}   {%domain%/dashboard/}  {200}
    html_footer.php     {deny}   {direct}   {%domain%/dashboard/}  {404}
    html_header.php     {deny}   {direct}   {%domain%/dashboard/}  {404}
    .htaccess           {deny}   {direct}   {%domain%/dashboard/}  {403}

The above list is actually an example of how I'd like the website to operate... The index.php file controls the requested 'files' and displays them accordingly. For example, %domain%/get-in-touch/ would display the contact.php file within the %domain%/includes/ directory. Calling index.php directly displays the same as %domain%/dashboard/.

The following code is from my .htaccess file. Please, ignore the # NN snippets. These are for your benefit to reduce the need for me to add code snippets. I suppose it would help you, too... You could directly reference the line numbers should you need to.

DirectoryIndex index.php                                                        # 01
Options All +FollowSymlinks -Indexes                                            # 02
                                                                                # 03
<IfModule mod_rewrite.c>                                                        # 04
    RewriteEngine On                                                            # 05
    #RewriteBase /rogue/                                                        # 06
                                                                                # 07
    RewriteCond %{REQUEST_FILENAME} !-d                                         # 08
    RewriteCond %{REQUEST_FILENAME} !-f                                         # 09
    RewriteRule ^(.*?)$ index.php [QSA,L]                                       # 10
                                                                                # 11
    RewriteCond %{REQUEST_FILENAME} -d                                          # 12
    RewriteRule ^(.*?)(js|image|css|includes|php|files)(.*)$ index.php [QSA,L]  # 13
</IfModule>                                                                     # 14

I commented out line 06 as the system didn't seem to need it, but I kept it for my own reference and in the off chance that I may need it later. Lines 08 through 10 control redirection to index.php when a file or a directory doesn't exist, and then the responding file controls the rest. The folders css/js/fonts/image/includes/php all exist, so lines 12 and 13 prevent access to them and redirect to index.php where the array of 'directories' can be located and the relevant files can be selected for inclusion. An example of the array is included at the bottom of the post. Lines 12 and 13 can be moved above line 08 and there would be no change, but removing them from the file causes a default 403 Forbidden error to fire.

Removing lines 08, 09, 12 and 13 causes the page to break by loading the files located in the image, files and fonts directories as MIME type text/html, however, the files within any other directories are seemingly unaffected.

Lines 12 and 13 perform as required, until you request access to a file within those directories. The file is displayed, and all of my secrecy is exposed to the world. As a result, certain files that have a great deal of importance are shown to the users. I want to prevent anybody from accessing the files within any folder I choose to deny access to, and have it redirect them to index.php and show a 403. I've attempted it by adding RewriteCond %{REQUEST_FILENAME} -f between lines 12 and 13, but the page shows as broken and fails to load resources.

NOTE: I had to remove -Indexes from line 02 as file requests from JavaScript files would be denied, doing this has also prevented lines 12 and 13 from doing as they were intended, regardless of their placement.

Directory Array [PHP]

$_INCDIRECTORY = 'includes/';
$_PATHINCLUDES = array(
    'dashboard'     => array(
        'allow'         => true,
        'path'          => $_INCDIRECTORY.'dashboard.php',
        // The following must remain FALSE; This value will be changed 'on-the-fly';
        'exists'        => false,
    ),
    'news'          => array(
        'allow'         => true,
        'path'          => $_INCDIRECTORY.'news.php',
        // The following must remain FALSE; This value will be changed 'on-the-fly';
        'exists'        => false,
    ),
    'search'        => array(
        'allow'         => true,
        'path'          => $_INCDIRECTORY.'search.php',
        // The following must remain FALSE; This value will be changed 'on-the-fly';
        'exists'        => false,
    ),
    // Excluded/Denied directories;
    'css'           => array(
        'allow'         => false,
        'path'          => 'css/',
        // The following must remain FALSE; This value will be changed 'on-the-fly';
        'exists'        => false,
    ),
    // Error pages;
    '403'           => array(
        'allow'         => true,
        'path'          => $_INCDIRECTORY.'errors/403.php',
        // The following must remain FALSE; This value will be changed 'on-the-fly';
        'exists'        => false,
    ),
    '404'           => array(
        'allow'         => true,
        'path'          => $_INCDIRECTORY.'errors/404.php',
        // The following must remain FALSE; This value will be changed 'on-the-fly';
        'exists'        => false,
    ),
);
  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答

1条回答 默认 最新

  • 已采纳
    douweihui0178 douweihui0178 2014-11-28 08:26

    After spending a while searching for a solution, and the usual trial and error attempts at solving it, I have come up with a solution that works exactly how I want it to. It seemed more fitting to answer my own question for reference to anybody who should come across the same issue.

    The following code rests within the .htaccess file at the root directory of your website. A description of what each line does follows after the snippet.

    The .htaccess Trickery

    DirectoryIndex index.php
    Options +FollowSymlinks -Indexes
    
    <IfModule mod_rewrite.c>
        # Enable mod_rewrite;
        RewriteEngine On
    
        # Detect if SSL Module is enabled;
        <IfModule mod_ssl.c>
            RewriteCond %{HTTPS} off
            RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,NC,L]
        </IfModule>
    
        # Check if the directory exists;
        RewriteCond %{REQUEST_FILENAME} -d
        RewriteCond %{HTTP_REFERER} !^((((https|http)://([w]{3}\.)?)([a-z0-9\.]+)/).*)$ [NC]
        RewriteRule ^(.*?)(js|image|css|includes|php|files)(.*)$ index.php [L]
    
        # Check if the file exists;
        RewriteCond %{REQUEST_FILENAME} -f
        RewriteCond %{HTTP_REFERER} !^((((https|http)://([w]{3}\.)?)([a-z0-9\.]+)/).*)$ [NC]
        RewriteRule ^(.*?)(js|image|css|includes|php|files)(.*)$ index.php [L]
    
        # Redirect to 'index.php', regardless of the request;
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteRule ^(.*?)$ index.php [QSA,L]
    </IfModule>
    

    DirectoryIndex index.php defines the primary 'homepage' of the website to index.php, this is not a requirement but it may be good practice to use this should your web-host change the default settings for Apache to use a different default index.

    Options +FollowSymlinks -Indexes does the following: +FollowSymlinks allows Apache to 'follow symbolic links', and -Indexes prevents any subfolders from being listed should an index.{ext} be missing. NOTE: Removing All from the previous configuration prevented script calls from being included within the exclusion, allowing all AJAX functionality.

    <IfModule mod_rewrite.c> ... </IfModule> allows the basis of the htaccess file to function without the Rewrite Module being enabled within your Apache config.

    RewriteEngine On enables the Rewrite Engine. Setting the value to Off disables this feature, and none of the underlying code will operate.

    <IfModule mod_ssl.c> ... </IfModule> allows the contained code to function, providing the Apache installation includes, and uses, the SSL Module.

    RewriteCond %{HTTPS} off then checks to see if %{HTTPS} is being used by the request, and then RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,NC,L] permanently redirects the user to an SSL enabled URI. %{HTTP_HOST}%{REQUEST_URI} simply allows the same functionality over multiple websites without the need of a reconfiguration.

    RewriteCond %{REQUEST_FILENAME} -d checks to see if the requested directory exists, RewriteCond %{HTTP_REFERER} !^((((https|http)://([w]{3}\.)?)([a-z0-9\.]+)/).*)$ [NC] then checks to see if the referrer was NOT requesting a full URL, basically a domain name. The RegEx following RewriteCond checks to see if the request uses http or https, optionally uses www. and then checks the rest of the URL. Then RewriteRule ^(.*?)(js|image|css|includes|php|files)(.*)$ index.php [L] checks to see if any of the folders requested were included within the RegEx and then redirects to index.php where the PHP code will handle the tricky stuff. NOTE: See this post for reference.

    Following the same course as the above code, RewriteCond %{REQUEST_FILENAME} -f does the same. But handles file requests, should they exist. NOTE: Again, see this post for reference.

    RewriteCond %{REQUEST_FILENAME} !-d and RewriteCond %{REQUEST_FILENAME} !-f check to see whether the requested file were a directory or a file and whether they existed, or not, and then RewriteRule ^(.*?)$ index.php [QSA,L] redirected the entire request to the index.php where the request would be correctly handled by PHP. NOTE: To those who didn't already know this, the ! character can be used in most cases as a shorthand for not. NOTE: See this page for a mod_rewrite variables cheatsheet. And this tool for RegEx testing.

    The PHP Magic

    Following the post, found here, to create your PHP file and then use the .htaccess config from within this answer would seemingly work perfectly, and provide the majority of the functionality that you may request. Severe modifications to the index have been made in my case, the more important ones are displayed below.

    switch( $path_info['call_parts'][0] ) {
        case 'about-us':
            include 'about.php';
        break;
        case 'users':
            include 'users.php';
        break;
        case 'news':
            include 'news.php';
        break;
        case 'products':
            include 'products.php';
        break;
        default:
            include 'front.php';
        break;
    }
    

    The above code can be changed to create a cleaner environment and an easier to understand configuration. Since some of the website functionality can be controlled by the requested files, changing the code to a more suitable alternative would be advised.

    For example: All of your requests can be added to an array that contains a few variables based on the request.

    $_INCDIRECTORY = 'includes/';
    $_PATHINCLUDES = array(
        'dashboard'     => array(
            'allow'         => true,
            'path'          => $_INCDIRECTORY.'dashboard.php',
            // The following must remain FALSE; This value will be changed 'on-the-fly';
            'exists'        => false,
        ),
        'news'          => array(
            'allow'         => true,
            'path'          => $_INCDIRECTORY.'news.php',
            // The following must remain FALSE; This value will be changed 'on-the-fly';
            'exists'        => false,
        ),
        'search'        => array(
            'allow'         => true,
            'path'          => $_INCDIRECTORY.'search.php',
            // The following must remain FALSE; This value will be changed 'on-the-fly';
            'exists'        => false,
        ),
        'register'      => array(
            'allow'         => true,
            'path'          => $_INCDIRECTORY.'register.php',
            // The following must remain FALSE; This value will be changed 'on-the-fly';
            'exists'        => false,
        ),
        ####################################
        ## DENIED PAGES/DIRECTORIES ########
        ####################################
        'css'           => array(
            'allow'         => false,
            'path'          => 'css/',
            // The following must remain FALSE; This value will be changed 'on-the-fly';
            'exists'        => false,
        ),
        'files'             => array(
            'allow'         => false,
            'path'          => 'files/',
            // The following must remain FALSE; This value will be changed 'on-the-fly';
            'exists'        => false,
        ),
        ####################################
        ## ERROR PAGES #####################
        ####################################
        '403'           => array(
            'allow'         => true,
            'path'          => $_INCDIRECTORY.'errors/403.php',
            // The following must remain FALSE; This value will be changed 'on-the-fly';
            'exists'        => false,
        ),
        '404'           => array(
            'allow'         => true,
            'path'          => $_INCDIRECTORY.'errors/404.php',
            // The following must remain FALSE; This value will be changed 'on-the-fly';
            'exists'        => false,
        ),
    );
    

    The initial array contains a list of each file, or directory, of the request. Each sub-array then contains request-specific variables that control the inclusion script later on. 'allow' defines whether the user is allowed to view the request. true would display the page, and false would display a 403 error. 'path' defines the file that is requested by the script, the file is checked by another snippet to see if it actually exists before being displayed when everything is green. 'exists' is always false at the beginning of the request, the following changes that.

    foreach( $_PATHINCLUDES as $key => &$value ) {
        ## Check to see if browsing the 'directory' is allowed
        ## before checking to see if the 'directory' exists;
        if( $value['allow'] == true ) {
            ## Browsing is allowed, check if 'directory'
            ## actually exists and set 'exists' to '=1';
            if( file_exists( $value['path'] ) == true ) {
                ## Set the value 'exists' to 'true';
                $value['exists'] = true;
            };
        } else {
            ## Browsing the 'directory' is not allowed,
            ## so the response should be a '403';
        };
    };
    unset( $value );
    

    The above code handles the array and sets 'exists' to true or false accordingly. Providing the 'allow' variable is true. There is no point in checking a folder/file the user can't access when its existence doesn't matter. .. as $key => &$value .. allows the value of the array to be changed within the foreach( .. ) loop, unset( $value ); removes the variable from being accessed by anything after the loop. NOTE: See this post for reference.

    To prevent any nasty PHP errors, such as requested offset [N] not set, etc... Use the following code. This checks that the variable actually exists before attempting to use it, if it doesn't exist, then it uses a predefined value.

    $_CALL_PART = isset( $path_info['call_parts'][0] ) ? $path_info['call_parts'][0] : 'dashboard';
    $_REQUESTED = isset( $_PATHINCLUDES[ $_CALL_PART ] ) ? $_PATHINCLUDES[ $_CALL_PART ] : $_PATHINCLUDES['404'];
    

    The final code snippet controls the behavior of the included file.

    ## Check to see if access to the file is
    ## allowed before doing anything else;
    if( $_REQUESTED['allow'] == true ) {
        ## The file can be browsed to, so check if it exists;
        if( $_REQUESTED['exists'] == true ) {
            ## The file exists, so include it;
            include( $_REQUESTED['path'] );
        } else {
            ## The file does not exist, so display a 404 page;
            if( $_PATHINCLUDES['404']['exists'] == true ) {
                ## Check to ensure the file exists before including it;
                ## This prevents an error;
                include( $_PATHINCLUDES['404']['path'] );
            } else {
                ## The file does not exist, so simply display a message;
                echo '<h1>404 - File Not Found: No ErrorDocument supplied</h1>';
            };
        };
    } else {
        ## The file cannot be browsed to, so display a 403;
        if( $_PATHINCLUDES['403']['exists'] == true ) {
            ## Check to ensure the file exists before including it;
            ## This prevents an error;
            include( $_PATHINCLUDES['403']['path'] );
        } else {
            ## The file does not exist, so simply display a message;
            echo '<h1>403 - Forbidden: No ErrorDocument supplied</h1>';
        };
    };
    

    if( $_REQUESTED['allow'] == true ) { ... } else { ... } checks to see whether the request is allowed. If so, the next section of the script can be executed. if( $_REQUESTED['exists'] == true ) { ... } else { ... } checks whether the requested file exists. If so, the script executes include( $_REQUESTED['path'] ); which displays the page. Otherwise, the script then returns a 404 which is subsequently checked in the same manner, excluding the allow parameter, and then displays it. If the prior if( .. ) statement was to return true, then the 403 error would be checked and displayed if the exists parameter returned as true. If neither the 403 and the 404 pages could be found, then the script would show a basic error response.

    点赞 评论 复制链接分享

相关推荐