Skip to main content
Average reading time: 14 minutes, 52 seconds
Total Votes: 1
Please Rate
Decorative image about how to secure a joomla site

4. Web server hardening

Start from Joomla's htaccess.txt

Rename htaccess.txt to .htaccess. Without it, Apache serves Joomla with no extra protection at all.

Tested: with the shipped file active, index.php?x=base64_encode(abc) and index.php?q=%3Cscript%3E returned 403, and responses gained X-Content-Type-Options: nosniff. But it does not hide version or distribution files. These were all still readable (200):

  • /administrator/manifests/files/joomla.xml (contains <version>6.1.4</version>)
  • /README.txt, /LICENSE.txt, /htaccess.txt, /web.config.txt
  • /language/en-GB/langmetadata.xml
  • /cli/joomla.php

Add these rules to the end of .htaccess

## ---- Additional hardening (tested on Joomla 6.1.4 / Apache 2.4.58) ----
# 1. Hide version-revealing and distribution files
<FilesMatch "(?i)^(README|LICENSE|htaccess|web\.config|configuration\.php-dist|robots\.txt\.dist)(\.txt)?$">
    Require all denied
</FilesMatch>
<IfModule mod_rewrite.c>
    RewriteRule ^administrator/manifests/ - [F,L]
    RewriteRule ^(cli|installation)/ - [F,L]
    RewriteRule ^(administrator/)?language/.+\.xml$ - [F,L]
</IfModule>

# 2. Never execute scripts from upload, cache, tmp and log folders
<IfModule mod_rewrite.c>
    RewriteRule ^(images|media|files|tmp|cache|administrator/cache|administrator/logs)/.*\.(php\d?|phtml|phar|pht|phps|cgi|pl|py|sh|asp|aspx|jsp)$ - [NC,F,L]
</IfModule>

# 3. Only if you do NOT use the Joomla Web Services API
<IfModule mod_rewrite.c>
    RewriteRule ^api(/|$) - [F,L]
</IfModule>

Tested: every file in the list above returned 403, including lowercase variants such as /readme.txt. Test PHP files placed in all seven folders returned 403, as did .PhAr, shell.jpg.php and files several subfolders deep. The home page, /administrator/, CSS/JS under /media, images and robots.txt all still returned 200. Joomla's own cache files under administrator/cache are included by PHP, never requested over HTTP, so the admin kept working.

The .phar rule matters on Ubuntu: its default PHP handler executes .phar and .phtml as well as .php.

The gap: a planted .htaccess turns rule 2 off

Rewrite rules are not inherited by a folder whose own .htaccess turns the rewrite engine on. In testing, an images/.htaccess containing only RewriteEngine On made images/zz_test.php execute again. An attacker who can write one file can usually write two, so rule 2 is defence in depth, not a fix.

If you control the server config, close the gap there. AllowOverride is ignored inside <DirectoryMatch> (tested: the planted .htaccess still worked), so each folder needs a plain <Directory> block:

# /etc/apache2/conf-available/joomla-noexec.conf  (adjust /var/www/html)
<Directory "/var/www/html/images">
    AllowOverride None
</Directory>
# ...repeat for files, media, tmp, cache, administrator/cache, administrator/logs

<DirectoryMatch "^/var/www/html/(images|files|media|tmp|cache|administrator/cache|administrator/logs)(/|$)">
    <FilesMatch "(?i)\.(php\d?|phtml|phar|pht|phps|cgi|pl|py|sh)$">
        Require all denied
    </FilesMatch>
    RemoveHandler .php .phtml .phar
    # mod_php only; remove the next line for PHP-FPM
    php_admin_flag engine off
</DirectoryMatch>

Enable it with a2enconf joomla-noexec && apachectl configtest && systemctl reload apache2.

Tested: with this in place, a planted images/.htaccess that set RewriteEngine On and mapped .jpg to PHP was ignored: .php files returned 403 and evil.jpg was served as plain text instead of running. The same held for a planted .htaccess several folders deep.

PHP settings

These go in the PHP configuration for the web server (for example /etc/php/8.3/apache2/conf.d/99-joomla.ini or the PHP-FPM pool):

expose_php = Off
display_errors = Off
log_errors = On
allow_url_include = Off
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec
open_basedir = /var/www/html:/var/joomla-private:/tmp
session.use_strict_mode = 1

Tested: with all of these active, the front end, admin login, Control Panel, System Information, Global Configuration, Media, Extensions → Install and Joomla Update screens all loaded with no errors. A test script confirmed system() was disabled and reading /etc/passwd was blocked by open_basedir. The CLI uses a separate php.ini, so cli/joomla.php is unaffected.

Check your own extensions before using disable_functions. Some backup and image tools call exec(); test on staging first.

Nginx

Joomla does not ship an Nginx file. The same intent translates to location blocks that deny all the paths in rule 1 and return 403 for script extensions under the writable folders. These Nginx rules were not part of this test run.

Share this article