skip to Main Content

Background

I am building a PHP application that uses MVC principles. I am not every experienced in writing .htaccess files, but I have a solution that works very well for my application. All requests are routed to a public/ directory for security and only absolute resources in this directory can be accessed, for example: http://localhost/public/css/main.css.

My issue is properly handling resource files (js, css, or images) that do not actually exist.

Issue & Question

My current .htaccess rules are causing an infinite loop and returning an http 500 status when a resource file (js, css, or images) does not exists.

I know I need to catch this file does not exists problem and return an http 404 status instead, but how do I do that without skipping the last 4 lines of my rules? I’m referring to the last 3 rewrite conditions and 1 rewrite rule. These are crucial to my app.

htaccess code

# Disable index view.
Options -Indexes

# Hide sensitive files.
<Files ~ ".(env|json|config|md|gitignore|gitattributes|lock|yml|xml)$">
    Order allow,deny
    Deny from all
</Files>

# Redirect all requests to the public directory.
<IfModule mod_rewrite.c>
    RewriteEngine On
    # Rewrite base if site is not at the servers root.
    RewriteBase /sub/directory/
    # If nothing or an index page is requested go to `public/index.php` and stop processing.
    RewriteRule ^(/||.*index..*)$ public/index.php [L,QSA]
    # If the URL already contains the `public/` directory go to URL and stop processing.
    RewriteRule ^(.*public/.*)$ $1 [L,QSA]
    # Does it appear that this URL is for a resource (JS, CSS, etc.)?
    # ▼ ▼ ▼ ISSUE STARTS HERE ▼ ▼ ▼
    RewriteCond %{REQUEST_URI} ^([wds-_/%]*).(?!php|phtml|htm|html).*$
    # Yes, prefix with `public/` and go to the modified URL.
    RewriteRule ^(.*)$ public/$1 [L,QSA]
    # Rewrite all remaining URLs to our apps MVC structure.
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-l
    RewriteRule ^([wds-_/%]*)$ public/index.php?p=$1 [L,QSA]
</IfModule>

2

Answers


  1. Chosen as BEST ANSWER

    arkascha's answer helped point me in the right direction; using END instead of L in the correct locations. For those looking to create a similar security focused MVC structure my working .htacces file is below. It provides the following benefits:

    • Directory listing is disabled and common sensitive files are forbidden from direct viewing. [A]
    • Any attempt to view an index page is redirected to the public directory. [B]
    • Rewrite loops to the public directory are caught and prevented from occurring. [C]
    • Direct access to resource files (JS, CSS, etc.) are allowed in the public directory. [D]
    • Missing resource files properly return a 404 instead of a 500. Thanks again to @arkascha for pointing out the difference between L and END in htacces files. [E]
    # Disable index view. [A]
    Options -Indexes
    
    # Hide sensitive files. [A]
    <Files ~ ".(env|json|config|md|gitignore|gitattributes|lock|yml|xml)$">
        Order allow,deny
        Deny from all
    </Files>
    
    # Redirect all requests to the public directory.
    <IfModule mod_rewrite.c>
        RewriteEngine On
        # Rewrite base if site is not at the servers root.
        RewriteBase /sub/directory/
        # If nothing or an index page is requested go to `public/index.php` and stop processing. [B]
        RewriteRule ^(/||.*index..*)$ public/index.php [END,QSA]
        # If the URL already contains the `public/` directory go to URL and stop processing. [C][E]
        RewriteRule ^(.*public/.*)$ $1 [END,QSA]
        # Does it appear that this URL is for a resource (JS, CSS, etc.)?
        RewriteCond %{REQUEST_URI} ^([wds-_/%]*).(?!php|phtml|htm|html).*$
        # Yes, prefix with `public/` and go to the modified URL. [D]
        RewriteRule ^(.*)$ public/$1 [L,QSA]
        # Rewrite all remaining URLs to our apps MVC structure. [E]
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteCond %{REQUEST_FILENAME} !-l
        RewriteRule ^([wds-_/%]*)$ public/index.php?p=$1 [END,QSA]
    </IfModule>
    

  2. Wrong approach in your question. You assume that you have to "catch" the file not found situation and return a 404. But that is not the case. Actually that is the default behavior of the apache http server so you’d have to do nothing for that. Your issue is that you implemented a rewriting loop for those requests, that is what you need to break. Then a 404 will get returned as a standard response.

    The actual issue is a classic one: your last rewrite rule rewrites to public/index.php. That target however is matched again by the pattern of the same rewrite rule. So your next rewriting target looks like public/index.php?p=public/index.php&p=some-page. Each time the rule rewrites the rewriting restarts, since the target has been altered. That behavior is documented.

    You have two options:

    Either you need to use the END flag instead of the L flag in that rule (to finally terminate the rewriting process for that request:

    RewriteRule ^([wds-_/%]*)$ public/index.php?p=$1 [END,QSA]
    

    Or you need to add an additional condition to break that loop:

    RewriteCond %{REQUEST_URI} !public/index.php$
    

    I would definitely recomment the first approach, assuming that there are no further rewriting steps interfering here.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search