Search code examples
phpapache.htaccessmod-rewrite

How to get .htaccess to return 404 instead of 500 for missing files


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} ^([\w\d\s\-\_\/\%]*)\.(?!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 ^([\w\d\s\-\_\/\%]*)$ public/index.php?p=$1 [L,QSA]
</IfModule>

Solution

  • 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} ^([\w\d\s\-\_\/\%]*)\.(?!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 ^([\w\d\s\-\_\/\%]*)$ public/index.php?p=$1 [END,QSA]
    </IfModule>