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>
CodePudding user response:
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 ^([\w\d\s\-\_\/\%]*)$ 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.
CodePudding user response:
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 thepublic
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 a500
. Thanks again to @arkascha for pointing out the difference betweenL
andEND
inhtacces
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>