php.htaccessconfigurationvaadin

.htaccess rules for filesmatch to protect config files


I have a .htaccess file like so:

#protect config files
<FilesMatch "\.(env|json|lock|xml|yml|htaccess|gitignore|gitattributes)$">
Order Allow,Deny
Deny from all
Require all denied
</FilesMatch>
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^([^\.]+)$ $1.php [NC,L]
</IfModule>
<filesMatch "\.(html|css|js)$">
    AddDefaultCharset UTF-8
</filesMatch>
<filesMatch "\.(css|js)$">
    FileETag MTime Size
</filesMatch>
ErrorDocument 304 /error_pages/304.php   
ErrorDocument 400 /error_pages/400.php   
ErrorDocument 401 /error_pages/401.php   
ErrorDocument 403 /error_pages/403.php 
ErrorDocument 404 /error_pages/404.php    
ErrorDocument 409 /error_pages/409.php   
ErrorDocument 410 /error_pages/410.php   
ErrorDocument 500 /error_pages/500.php
<IfModule mod_expires.c>
    ExpiresActive On
    # Fonts
    ExpiresByType font/ttf "access plus 1 year"
    ExpiresByType font/otf "access plus 1 year"
    ExpiresByType font/woff "access plus 1 year"
    ExpiresByType font/woff2 "access plus 1 year"
    # Images
    ExpiresByType image/jpg "access plus 1 year"
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/gif "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType image/x-icon "access plus 6 year"
    # CSS, JavaScript
    ExpiresByType text/css "access plus 6 month"
    ExpiresByType application/javascript "access plus 6 month"
    ExpiresByType application/x-javascript "access plus 6 month"
    ExpiresByType application/x-shockwave-flash "access plus 6 month"
    # Other
    ExpiresByType application/pdf "access plus 6 month"
    ExpiresDefault "access plus 10 days"
</IfModule>
<IfModule mod_headers.c>
    # Jaar Public
    <FilesMatch "\.(jpg|jpeg|png|gif|svg|webp|ico|ttf|woff|woff2|JPG|JPEG|PNG|GIF|SVG|WEBP|ICO|TTF|WOFF|WOFF2)$">
        Header set Cache-Control "max-age=31536000, public"
    </FilesMatch>

    # Jaar
    <FilesMatch "\.(js|css|swf)$">
        Header set Cache-Control "max-age=31536000"
    </FilesMatch>
</IfModule>
# Disable directory browsing
Options All -Indexes

How can I still access json files in a subfolder? Some part of i18n is in a subfolder called assets and is now not loaded in because of the first directive. As I am a front-end person I don't know a lot about Apache. I also have problem with the JS routing from Vaadin (see: Vaadin/Router) On reload of a link within the page like so: /page/123 it doesn't load, instead returning a 404. When returning to /page it works as intended. I have found that /page.php/123 works as intended if configured so. Is it possible to hide the .php extension, but still request the page.php with the /123 URI?


Solution

  • #protect config files
    <FilesMatch "\.(env|json|lock|xml|yml|htaccess|gitignore|gitattributes)$">
    Order Allow,Deny
    Deny from all
    Require all denied
    </FilesMatch>
    

    The fact that this started working after removing the <IfModule mod_headers.c> container implies that mod_headers is not installed on your system*1. Which is a little unusual. (Although it made no sense to contain that block in such an <IfModule> directive anyway. In fact, you should generally avoid <IfModule> directives in your config, unless the contained rules are optional and the same config is intended to be used unaltered on many different systems. Otherwise, the <IfModule> directive simply masks the error.)

    You are also mixing both Apache 2.2 (Order and Deny) directives with Apache 2.4 (Require all denied) directive. This is bad practice and can result in unexpected conflicts (not to mention that Order and Deny are formerly deprecated and not available by default on Apache 2.4). So, using this same config on other systems is likely to fail. Assuming you do not have other Order, Deny and/or Allow directives in your config (anywhere) then you should remove the Order and Deny directives here.

    (*1 If mod_headers is not installed then the later section that sets some Cache-Control headers is also not processed. Although this is probably a good thing, since this section conflicts with the mod_expires section directly above it!?)

    Json files are blocked, but then also in subfolders. I only want to block project configuration files in the root, for security reasons.

    ("for security reasons"?! Presumably it's because your application needs client-side access to .json files in subdirectories for some reason? Not a security thing, as allowing access can only be less secure?)

    The <FilesMatch> directive matches against the filename only, not the file-path, so applies to all subdirectories (which is generally what you want in this scenario).

    If you want to be able to access .json files in subdirectories then you need to remove |json from the above alternation section in the regex. And create a separate rule that blocks requests for .json files in the root only. For example:

    # Block requests for ".json" files in the root only
    <If "%{REQUEST_URI} =~ m#^/[^/]+\.json$#">
        Require all denied
    </If>
    

    This uses an Apache expression to examine the requested URL. Note that this blocks URLs, regardless of whether a .json file actually exists. (Which is actually the same as the <FilesMatch> directive.)

    The regex ^/[^/]+\.json$ will only match /<something>.json and not /<something>/<something>.json etc.

    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^([^\.]+)$ $1.php [NC,L]
    

    For example the route { path: "/tickets.php/:id", component: "ticket-component" } works when the hotlinks are updated to tickets.php/4509 instead of tickets/4509

    This simply appends the .php extension to any URL-path that does not contain a dot and that does not map to a file*2. (It should at least check that the target .php file exists.)

    (*2 Note that this does not necessarily test the full URL-path since REQUEST_FILENAME is a calculated value based on your filesystem.)

    With the above rule, if you request /tickets/4509 (and /tickets is not a physical subdirectory) then the RewriteRule directive first matches against tickets/4509 (which is saved in the $1 backreference). The preceding condition then tests if /tickets (not /tickets/4509) is not a physical file (which I assume it's not, so the condition is successful) so ends up rewriting the request to tickets/4509.php - which naturally fails with a 404.

    Assuming all your relevant .php files exist in the document root only (not subdirectories) then you would need to write the rule something like the following instead to test for .php files in root and append the remaining URL-path as path-info (which seems to be what you are expecting).

    For example:

    # Rewrite requests of the form "/page/id" to "/page.php/id"
    RewriteCond %{DOCUMENT_ROOT}/$1.php -f
    RewriteRule ^([^./]+)(/\d+)?$ $1.php$2 [L]
    

    Note that I included a slash in the negated regex character class so it only matches the first URL-path segment, not the entire URL-path.

    I'm assuming (based on your examples) that only numeric IDs are being passed. eg. /<page>/<numeric-id>. But I've allowed for the /<numeric-id> part to be entirely optional, so /tickets only could be requested and is simply rewritten to /tickets.php.

    The preceding condition (RewriteCond directive) now checks that the corresponding .php file actually exists before attempting to rewrite the request.

    No need to backslash-escape literal dots when used inside a regex character class. And the NC flag is superfluous, since the regex itself is not case-specific.

    Summary

    So, bringing the above points together, your complete .htaccess file would now look like this:

    # Disable directory browsing
    Options All -Indexes
    
    # Block requests for ".json" files in the root only
    <If "%{REQUEST_URI} =~ m#^/[^/]+\.json$#">
        Require all denied
    </If>
    
    # Protect other config files (anywhere)
    <FilesMatch "\.(env|lock|xml|yml|htaccess|gitignore|gitattributes)$">
        Require all denied
    </FilesMatch>
    
    <FilesMatch "\.(html|css|js)$">
        AddDefaultCharset UTF-8
    </FilesMatch>
    <FilesMatch "\.(css|js)$">
        FileETag MTime Size
    </FilesMatch>
    
    ErrorDocument 304 /error_pages/304.php   
    ErrorDocument 400 /error_pages/400.php   
    ErrorDocument 401 /error_pages/401.php   
    ErrorDocument 403 /error_pages/403.php 
    ErrorDocument 404 /error_pages/404.php    
    ErrorDocument 409 /error_pages/409.php   
    ErrorDocument 410 /error_pages/410.php   
    ErrorDocument 500 /error_pages/500.php
    
    <IfModule mod_expires.c>
        ExpiresActive On
        # Fonts
        ExpiresByType font/ttf "access plus 1 year"
        ExpiresByType font/otf "access plus 1 year"
        ExpiresByType font/woff "access plus 1 year"
        ExpiresByType font/woff2 "access plus 1 year"
        # Images
        ExpiresByType image/jpg "access plus 1 year"
        ExpiresByType image/jpeg "access plus 1 year"
        ExpiresByType image/gif "access plus 1 year"
        ExpiresByType image/png "access plus 1 year"
        ExpiresByType image/x-icon "access plus 6 year"
        # CSS, JavaScript
        ExpiresByType text/css "access plus 6 month"
        ExpiresByType application/javascript "access plus 6 month"
        ExpiresByType application/x-javascript "access plus 6 month"
        ExpiresByType application/x-shockwave-flash "access plus 6 month"
        # Other
        ExpiresByType application/pdf "access plus 6 month"
        ExpiresDefault "access plus 10 days"
    </IfModule>
    
    <IfModule mod_rewrite.c>
        RewriteEngine On
    
        # Rewrite requests of the form "/page/id" to "/page.php/id"
        RewriteCond %{DOCUMENT_ROOT}/$1.php -f
        RewriteRule ^([^./]+)(/\d+)?$ $1.php$2 [L]
    </IfModule>
    

    I also adjusted the logical order of the rule blocks, which helps readability and maintenance (although this does not strictly affect functionality).

    Ordinarily, the <IfModule mod_rewrite.c> wrapper should probably be removed, although your system is technically usable without this section, providing you use (the less pretty) URLs of the form /page.php/<id>.

    Although this is still strictly not complete as URLs of the form /page.php/<id> are still accessible, potentially creating a duplicate content issue (whether this would become a real problem for your site is another matter). You could implement this as a canonical redirect in .htaccess (or your PHP code) or simply block such requests with a 404. This is left as an exercise for the reader.