shell bypass 403
<?php
/**
* @package admintools
* @copyright Copyright (c)2010-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Component\AdminTools\Administrator\Model;
defined('_JEXEC') or die;
use Akeeba\Component\AdminTools\Administrator\Helper\CloudIPRanges;
use Akeeba\Component\AdminTools\Administrator\Helper\ServerTechnology;
use DateTimeZone;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
#[\AllowDynamicProperties]
class HtaccessmakerModel extends ServerconfigmakerModel
{
use ApacheVersionTrait;
/**
* The current configuration of this feature
*
* @var object
*/
protected $configKey = 'htconfig';
/**
* The base name of the configuration file being saved by this feature, e.g. ".htaccess". The file is always saved
* in the site's root. Any old files under that name are renamed with a .admintools suffix.
*
* @var string
*/
protected $configFileName = '.htaccess';
/** @inheritdoc */
public function isSupported(): int
{
return ServerTechnology::isHtaccessSupported();
}
/** @inheritdoc */
public function hasPhpHandlers(): bool
{
$htaccess_file = JPATH_ROOT.'/.htaccess';
if (!file_exists($htaccess_file))
{
return false;
}
$contents = file_get_contents($htaccess_file);
if ($contents === false)
{
return false;
}
return !is_null($this->extractHandler($contents));
}
/** @inheritdoc */
public function getPhpHandlers(): ?string
{
$htaccess_file = JPATH_ROOT.'/.htaccess';
if (!file_exists($htaccess_file))
{
return null;
}
$contents = @file_get_contents($htaccess_file);
if ($contents === false)
{
return null;
}
return $this->extractHandler($contents);
}
/**
* This function will copy the handlers from the main .htaccess file and it will save it inside the custom rules field
*
* @return void
* @throws \Exception
*/
public function includePhpHandlers()
{
$htaccess_file = JPATH_ROOT.'/.htaccess';
if (!file_exists($htaccess_file))
{
throw new \RuntimeException(Text::_('COM_ADMINTOOLS_HTACCESSMAKER_LBL_PHPHANDLERS_ERR_NO_HTACCESS'));
}
$contents = file_get_contents($htaccess_file);
$handlers = $this->extractHandler(is_string($contents) ? $contents : '');
if (!$handlers)
{
throw new \RuntimeException(Text::_('COM_ADMINTOOLS_HTACCESSMAKER_LBL_PHPHANDLERS_ERR_NO_HANDLERS'));
}
// Be sure we do have a loaded configuration
$data = $this->loadConfiguration();
// Double check that we do not already have some handlers in the custom footer field
if ($data['custfoot'])
{
$handlers_footer = $this->extractHandler($data['custfoot']);
if ($handlers_footer)
{
throw new \RuntimeException(Text::_('COM_ADMINTOOLS_HTACCESSMAKER_LBL_PHPHANDLERS_ERR_ALREADY_SAVED'));
}
}
$data['custfoot'] .= "\n".$handlers;
$this->saveConfiguration($data);
}
/** @inheritdoc */
public function extractHandler(string $server_config): ?string
{
// Normalize the .htaccess
$server_config = $this->normalizeHtaccess($server_config);
// Look for SetHandler and AddHandler in Files and FilesMatch containers
foreach (['Files', 'FilesMatch'] as $container)
{
$result = $this->extractContainer($container, $server_config);
if (!is_null($result))
{
return $result;
}
}
// Fallback: extract an AddHandler line
$found = preg_match('#^AddHandler\s?.*\.php.*$#mi', $server_config, $matches);
if ($found >= 1)
{
return $matches[0];
}
return null;
}
/**
* Normalize the .htaccess file content, making it suitable for handler extraction
*
* @param string $htaccess The original file
*
* @return string The normalized file
*/
private function normalizeHtaccess(string $htaccess): string
{
// Convert all newlines into UNIX style
$htaccess = str_replace("\r\n", "\n", $htaccess);
$htaccess = str_replace("\r", "\n", $htaccess);
// Squash whitespace
$htaccess = preg_replace('/[\040\011]{1,}/m', ' ', $htaccess);
$htaccess = preg_replace('/^[\040\011]{1,}/m', '', $htaccess);
// Return only non-comment, non-empty lines
$isNonEmptyNonComment = function ($line) {
$line = trim($line);
return !empty($line) && (substr($line, 0, 1) !== '#');
};
$lines = array_map('trim', explode("\n", $htaccess));
return implode("\n", array_filter($lines, $isNonEmptyNonComment));
}
/**
* Extracts a Files or FilesMatch container with an AddHandler or SetHandler line
*
* @param string $container "Files" or "FilesMatch"
* @param string $htaccess The .htaccess file content
*
* @return string|null NULL when not found
*/
protected function extractContainer(string $container, string $htaccess): ?string
{
// Try to find the opening container tag e.g. <Files....>
$pattern = sprintf('#<%s\s*.*\.php.*>#m', $container);
$found = preg_match_all($pattern, $htaccess, $matches, PREG_OFFSET_CAPTURE);
if (!$found)
{
return null;
}
foreach ($matches[0] as $thisMatch)
{
// Get the rest of the .htaccess sample
$openContainer = $thisMatch[0];
$subsetHtaccess = trim(substr($htaccess, $thisMatch[1] + strlen($thisMatch[0])));
// Try to find the closing container tag
$pattern = sprintf('#<\s*/%s.*>#m', $container);
$innerFound = preg_match($pattern, $subsetHtaccess, $innerMatches, PREG_OFFSET_CAPTURE);
if (!$innerFound)
{
continue;
}
// Get the rest of the .htaccess sample
$subsetHtaccess = trim(substr($subsetHtaccess, 0, $innerMatches[$innerFound - 1][1]));
$closeContainer = $innerMatches[$innerFound - 1][0];
if (empty($subsetHtaccess))
{
continue;
}
// Now we'll explode remaining lines and find the first SetHandler or AddHandler line
$lines = array_map('trim', explode("\n", $subsetHtaccess));
$lines = array_filter(
$lines,
function ($line) {
$lowercaseLine = strtolower($line);
return (strpos($lowercaseLine, 'addhandler') === 0)
|| (strpos($lowercaseLine, 'sethandler') === 0)
|| (strpos($lowercaseLine, 'fcgid') === 0);
}
);
if (empty($lines))
{
continue;
}
return $openContainer . "\n" . array_shift($lines) . "\n" . $closeContainer;
}
return null;
}
/**
* Compile and return the contents of the .htaccess configuration file
*
* @return string
*/
public function makeConfigFile()
{
// Make sure we are called by an expected caller
ServerTechnology::checkCaller($this->allowedCallersForMake);
$app = Factory::getApplication();
// Guess Apache features
$apacheVersion = $this->apacheVersion();
$serverCaps = (object) [
'customCodes' => version_compare($apacheVersion, '2.2', 'ge'), // Custom redirections, e.g. R=301
'deflate' => version_compare($apacheVersion, '2.0', 'ge') // mod_deflate support
];
$redirCode = $serverCaps->customCodes ? '[R=301,L]' : '[R,L]';
$timezone = 'UTC';
// Fetch the timezone from the user only if we're not in CLI
if (!$app->isClient('cli'))
{
$timezone = $app->getIdentity()->getParam('timezone', $app->get('offset', 'UTC'));
}
$date = clone Factory::getDate();
$tz = new DateTimeZone($timezone);
$date->setTimezone($tz);
$d = $date->format('Y-m-d H:i:s T', true);
$version = ADMINTOOLS_VERSION;
$htaccess = <<<END
### ===========================================================================
### Security Enhanced & Highly Optimized .htaccess File for Joomla!
### automatically generated by Admin Tools $version on $d
### Auto-detected Apache version: $apacheVersion (best guess)
### ===========================================================================
###
### The contents of this file are based on the same author's work "Master
### .htaccess".
###
### Admin Tools is Free Software, distributed under the terms of the GNU
### General Public License version 3 or, at your option, any later version
### published by the Free Software Foundation.
###
### !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
### !! !!
### !! If you get an Internal Server Error 500 or a blank page when trying !!
### !! to access your site, remove this file and try tweaking its settings !!
### !! in the back-end of the Admin Tools component. !!
### !! !!
### !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
###
END;
$config = (object) $this->loadConfiguration();
// Let's start with IP restriction
$restrictIP = $config->restrictip ?? 'none';
if ($restrictIP !== 'none')
{
$restrictIPList = CloudIPRanges::getIPRanges($restrictIP);
if ($restrictIPList === null)
{
throw new \RuntimeException(Text::_('COM_ADMINTOOLS_ERR_INVALID_RESTRICTIP'));
}
$isApache24 = version_compare($apacheVersion, '2.4', 'ge');
$htaccess .= <<< END
##### Restricted access by IP address -- BEGIN
END;
if (!$isApache24)
{
$htaccess .= <<<END
Order Deny,Allow
Deny from all
END;
}
foreach ($restrictIPList as $ip)
{
$htaccess .= ($isApache24 ? 'Require ip ' : 'Allow from ').$ip."\n";
}
$htaccess .= <<< END
##### Restricted access by IP address -- END
END;
}
// Is HSTS enabled?
$hasHSTS = $config->hstsheader != 0;
$htaccess .= <<< HTACCESS
##### RewriteEngine enabled - BEGIN
RewriteEngine On
##### RewriteEngine enabled - END
# PHP FastCGI fix for HTTP Authorization
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
HTACCESS;
$rewritebase = $config->rewritebase;
if (!empty($rewritebase))
{
$htaccess .= "##### RewriteBase set - BEGIN\n";
$rewritebase = trim($rewritebase, '/');
$htaccess .= "RewriteBase /$rewritebase\n";
$htaccess .= "##### RewriteBase set - END\n\n";
}
if ($hasHSTS)
{
$httpsHost = $config->httpshost;
$htaccess .= <<< END
##### HTTP to HTTPS redirection
## Since you have enabled HSTS the first redirection rule will instruct the browser to visit the HTTPS version of your
## site. This prevents unsafe redirections through HTTP.
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteRule .* https://$httpsHost%{REQUEST_URI} [L,R=301]
END;
}
if (!empty($config->custhead))
{
$htaccess .= "##### Custom Rules (Top of File) -- BEGIN\n";
$htaccess .= $config->custhead . "\n";
$htaccess .= "##### Custom Rules (Top of File) -- END\n\n";
}
if ($config->fileorder == 1)
{
$htaccess .= "##### File execution order -- BEGIN\n";
$htaccess .= "DirectoryIndex index.php index.html\n";
$htaccess .= "##### File execution order -- END\n\n";
}
if ($config->nodirlists == 1)
{
$htaccess .= "##### No directory listings -- BEGIN\n";
$htaccess .= "IndexIgnore *\n";
switch ($config->symlinks)
{
case 0:
$htaccess .= "Options -Indexes\n";
break;
case 1:
$htaccess .= "Options -Indexes +FollowSymLinks\n";
break;
case 2:
$htaccess .= "Options -Indexes +SymLinksIfOwnerMatch\n";
break;
}
$htaccess .= "##### No directory listings -- END\n\n";
}
elseif ($config->symlinks != 0)
{
$htaccess .= "##### Follow symlinks -- BEGIN\n";
switch ($config->symlinks)
{
case 1:
$htaccess .= "Options +FollowSymLinks\n";
break;
case 2:
$htaccess .= "Options +SymLinksIfOwnerMatch\n";
break;
}
$htaccess .= "##### Follow symlinks -- END\n\n";
}
if ($config->exptime != 0)
{
$expWeek = '1 week';
$expMonth = '1 month';
if ($config->exptime == 2)
{
$expWeek = '1 year';
$expMonth = '1 year';
}
$htaccess .= <<<END
##### Optimal default expiration time - BEGIN
<IfModule mod_expires.c>
# Enable expiration control
ExpiresActive On
# No caching for specific resource types
## -- Application cache manifest
ExpiresByType text/cache-manifest "now"
## -- XML and JSON
ExpiresByType application/json "now"
ExpiresByType application/xml "now"
ExpiresByType text/xml "now"
## RSS and Atom feeds: 1 hour (hardcoded)
ExpiresByType application/atom+xml "now plus 1 hour"
ExpiresByType application/rss+xml "now plus 1 hour"
# CSS and JS expiration: $expWeek after request
ExpiresByType text/css "now plus $expWeek"
ExpiresByType text/javascript "now plus $expWeek"
ExpiresByType application/javascript "now plus $expWeek"
ExpiresByType application/ld+json "now plus $expWeek"
ExpiresByType application/x-javascript "now plus $expWeek"
# Image files expiration: $expMonth after request
ExpiresByType application/ico "now plus $expMonth"
ExpiresByType application/smil "now plus $expMonth"
ExpiresByType application/vnd.wap.wbxml "now plus $expMonth"
ExpiresByType image/bmp "now plus $expMonth"
ExpiresByType image/gif "now plus $expMonth"
ExpiresByType image/ico "now plus $expMonth"
ExpiresByType image/icon "now plus $expMonth"
ExpiresByType image/jp2 "now plus $expMonth"
ExpiresByType image/jpeg "now plus $expMonth"
ExpiresByType image/jpg "now plus $expMonth"
ExpiresByType image/pipeg "now plus $expMonth"
ExpiresByType image/png "now plus $expMonth"
ExpiresByType image/svg+xml "now plus $expMonth"
ExpiresByType image/tiff "now plus $expMonth"
ExpiresByType image/vnd.microsoft.icon "now plus $expMonth"
ExpiresByType image/vnd.wap.wbmp "now plus $expMonth"
ExpiresByType image/webp "now plus $expMonth"
ExpiresByType image/x-icon "now plus $expMonth"
ExpiresByType text/ico "now plus $expMonth"
# Font files expiration: $expWeek after request
ExpiresByType application/font-woff "now plus $expWeek"
ExpiresByType application/font-woff2 "now plus $expWeek"
ExpiresByType application/vnd.ms-fontobject "now plus $expWeek"
ExpiresByType application/x-font-opentype "now plus $expWeek"
ExpiresByType application/x-font-ttf "now plus $expWeek"
ExpiresByType application/x-font-woff "now plus $expWeek"
ExpiresByType font/opentype "now plus $expWeek"
ExpiresByType font/otf "now plus $expWeek"
ExpiresByType font/ttf "now plus $expWeek"
ExpiresByType font/woff "now plus $expWeek"
ExpiresByType font/woff2 "now plus $expWeek"
# Audio files expiration: $expMonth after request
ExpiresByType application/ogg "now plus $expMonth"
ExpiresByType audio/3gpp "now plus $expMonth"
ExpiresByType audio/3gpp2 "now plus $expMonth"
ExpiresByType audio/aac "now plus $expMonth"
ExpiresByType audio/basic "now plus $expMonth"
ExpiresByType audio/mid "now plus $expMonth"
ExpiresByType audio/midi "now plus $expMonth"
ExpiresByType audio/mp3 "now plus $expMonth"
ExpiresByType audio/mpeg "now plus $expMonth"
ExpiresByType audio/ogg "now plus $expMonth"
ExpiresByType audio/opus "now plus $expMonth"
ExpiresByType audio/x-aiff "now plus $expMonth"
ExpiresByType audio/x-mpegurl "now plus $expMonth"
ExpiresByType audio/x-pn-realaudio "now plus $expMonth"
ExpiresByType audio/x-wav "now plus $expMonth"
ExpiresByType audio/wav "now plus $expMonth"
# Movie files expiration: $expMonth after request
ExpiresByType application/x-shockwave-flash "now plus $expMonth"
ExpiresByType video/3gpp "now plus $expMonth"
ExpiresByType video/3gpp2 "now plus $expMonth"
ExpiresByType video/mp4 "now plus $expMonth"
ExpiresByType video/mpeg "now plus $expMonth"
ExpiresByType video/ogg "now plus $expMonth"
ExpiresByType video/quicktime "now plus $expMonth"
ExpiresByType video/webm "now plus $expMonth"
ExpiresByType video/x-la-asf "now plus $expMonth"
ExpiresByType video/x-ms-asf "now plus $expMonth"
ExpiresByType video/x-msvideo "now plus $expMonth"
ExpiresByType x-world/x-vrml "now plus $expMonth"
</IfModule>
# Disable caching of administrator/index.php
<Files "administrator/index.php">
<IfModule mod_expires.c>
ExpiresActive Off
</IfModule>
<IfModule mod_headers.c>
Header unset ETag
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT"
</IfModule>
</Files>
##### Optimal default expiration time - END
END;
}
if (!empty($config->hoggeragents) && ($config->nohoggers == 1))
{
$htaccess .= "##### Common hacking tools and bandwidth hoggers block -- BEGIN\n";
$htaccess .= "<IfModule mod_setenvif.c>\n";
foreach ($config->hoggeragents as $agent)
{
$htaccess .= "SetEnvIf user-agent \"(?i:$agent)\" stayout=1\n";
}
$htaccess .= <<< HTACCESS
<IfModule !mod_authz_core.c>
deny from env=stayout
</IfModule>
<IfModule mod_authz_core.c>
<RequireAll>
Require all granted
Require not env stayout
</RequireAll>
</IfModule>
##### Common hacking tools and bandwidth hoggers block -- END
HTACCESS;
$htaccess .= "</IfModule>\n";
}
if (($config->autocompress == 1) && ($serverCaps->deflate))
{
// See https://stackoverflow.com/questions/5230202/apache-addoutputfilterbytype-is-deprecated-how-to-rewrite-using-mod-filter
$apacheModuleForDeflate = version_compare($apacheVersion, '2.4', 'ge') ? 'mod_filter' : 'mod_deflate';
$htaccess .= <<<HTACCESS
##### Automatic compression of resources -- BEGIN
# Automatically serve .css.gz, .css.br, .js.gz or .js.br instead of the original file
# These are versions of the files pre-compressed with GZip or Brotli, respectively
<IfModule mod_headers.c>
# Serve Brotli compressed CSS files if they exist and the client accepts Brotli.
RewriteCond "%{HTTP:Accept-encoding}" "br"
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.*)\.css" "$1\.css\.br" [QSA]
# Serve Brotli compressed JS files if they exist and the client accepts Brotli.
RewriteCond "%{HTTP:Accept-encoding}" "br"
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.*)\.js" "$1\.js\.br" [QSA]
# Serve correct content types, and prevent double compression.
RewriteRule "\.css\.br$" "-" [T=text/css,E=no-gzip:1,E=no-brotli:1,L]
RewriteRule "\.js\.br$" "-" [T=text/javascript,E=no-gzip:1,E=no-brotli:1,L]
<FilesMatch "(\.js\.br|\.css\.br)$">
# Serve correct encoding type.
Header set Content-Encoding br
# Force proxies to cache gzipped & non-gzipped css/js files separately.
Header append Vary Accept-Encoding
</FilesMatch>
# Serve gzip compressed CSS files if they exist and the client accepts gzip.
RewriteCond "%{HTTP:Accept-encoding}" "gzip"
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.*)\.css" "$1\.css\.gz" [QSA]
# Serve gzip compressed JS files if they exist and the client accepts gzip.
RewriteCond "%{HTTP:Accept-encoding}" "gzip"
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.*)\.js" "$1\.js\.gz" [QSA]
# Serve correct content types, and prevent $apacheModuleForDeflate double gzip.
# Also set it as the last rule to prevent the Front- or Backend protection from preventing access to the .gz file.
RewriteRule "\.css\.gz$" "-" [T=text/css,E=no-gzip:1,E=no-brotli:1,L]
RewriteRule "\.js\.gz$" "-" [T=text/javascript,E=no-gzip:1,E=no-brotli:1,L]
<FilesMatch "(\.js\.gz|\.css\.gz)$">
# Serve correct encoding type.
Header set Content-Encoding gzip
# Force proxies to cache gzipped & non-gzipped css/js files separately.
Header append Vary Accept-Encoding
</FilesMatch>
</IfModule>
## Automatically compress by MIME type using mod_brotli. Takes priority due to better compression ratio.
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/plain text/xml text/css application/xml application/xhtml+xml application/rss+xml application/javascript application/x-javascript text/javascript image/svg+xml
</IfModule>
## Automatically compress by MIME type using {$apacheModuleForDeflate}.
<IfModule {$apacheModuleForDeflate}.c>
AddOutputFilterByType DEFLATE text/plain text/xml text/css application/xml application/xhtml+xml application/rss+xml application/javascript application/x-javascript text/javascript image/svg+xml
</IfModule>
## Fallback to mod_gzip when neither mod_brotli nor $apacheModuleForDeflate is available
<IfModule !mod_brotli.c>
<IfModule !{$apacheModuleForDeflate}.c>
<IfModule mod_gzip.c>
mod_gzip_on Yes
mod_gzip_dechunk Yes
mod_gzip_keep_workfiles No
mod_gzip_can_negotiate Yes
mod_gzip_add_header_count Yes
mod_gzip_send_vary Yes
mod_gzip_min_http 1000
mod_gzip_minimum_file_size 300
mod_gzip_maximum_file_size 512000
mod_gzip_maximum_inmem_size 60000
mod_gzip_handle_methods GET
mod_gzip_item_include file \.(html?|txt|css|js|php|pl|xml|rb|py|svg|scgz)$
mod_gzip_item_include mime ^text/javascript$
mod_gzip_item_include mime ^text/plain$
mod_gzip_item_include mime ^text/xml$
mod_gzip_item_include mime ^text/css$
mod_gzip_item_include mime ^application/xml$
mod_gzip_item_include mime ^application/xhtml+xml$
mod_gzip_item_include mime ^application/rss+xml$
mod_gzip_item_include mime ^application/javascript$
mod_gzip_item_include mime ^application/x-javascript$
mod_gzip_item_include mime ^image/svg+xml$
mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.*
mod_gzip_item_include handler ^cgi-script$
mod_gzip_item_include handler ^server-status$
mod_gzip_item_include handler ^server-info$
mod_gzip_item_include handler ^application/x-httpd-php
mod_gzip_item_exclude mime ^image/.*
</ifmodule>
</IfModule>
</IfModule>
##### Automatic compression of resources -- END
HTACCESS;
if ($config->forcegzip == 1)
{
$htaccess .= <<< HTACCESS
## Force GZip compression for mangled Accept-Encoding headers
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
</IfModule>
</IfModule>
HTACCESS;
}
}
if ($config->etagtype != 'default')
{
$htaccess .= "## Send ETag (selected method: {$config->etagtype})\n";
switch ($config->etagtype)
{
case 'full':
$htaccess .= <<< HTACCESS
FileETag All
HTACCESS;
break;
case 'sizetime':
$htaccess .= <<< HTACCESS
FileETag MTime Size
HTACCESS;
break;
case 'size':
$htaccess .= <<< HTACCESS
FileETag Size
HTACCESS;
break;
case 'none':
$htaccess .= <<< HTACCESS
<IfModule mod_headers.c>
Header unset ETag
</IfModule>
FileETag None
HTACCESS;
break;
}
}
if ($config->autoroot)
{
$htaccess .= <<<END
##### Redirect index.php to / -- BEGIN
RewriteCond %{THE_REQUEST} !^POST
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index\.php\ HTTP/
RewriteRule ^index\.php$ / $redirCode
##### Redirect index.php to / -- END
END;
}
// If I have a rewriteBase condition, I have to append it here
$subfolder = trim($config->rewritebase, '/') ? trim($config->rewritebase, '/') . '/' : '';
switch ($config->wwwredir)
{
// non-www to www
case 1:
if ($hasHSTS)
{
$htaccess .= <<<END
##### Redirect non-www to www -- BEGIN
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.%{HTTP_HOST}/$subfolder$1 $redirCode
##### Redirect non-www to www -- END
END;
}
else
{
{
$htaccess .= <<<END
##### Redirect non-www to www -- BEGIN
# HTTP
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ http://www.%{HTTP_HOST}/$subfolder$1 $redirCode
# HTTPS
RewriteCond %{HTTPS} =on [OR]
RewriteCond %{HTTP:X-Forwarded-Proto} ==https
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.%{HTTP_HOST}/$subfolder$1 $redirCode
##### Redirect non-www to www -- END
END;
}
}
break;
// www to non-www
case 2:
if ($hasHSTS)
{
$htaccess .= <<<END
##### Redirect www to non-www -- BEGIN
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$subfolder$1 $redirCode
##### Redirect www to non-www -- END
END;
}
else
{
$htaccess .= <<<END
##### Redirect www to non-www -- BEGIN
# HTTP
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ http://%1/$subfolder$1 $redirCode
# HTTPS
RewriteCond %{HTTPS} =on [OR]
RewriteCond %{HTTP:X-Forwarded-Proto} ==https
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$subfolder$1 $redirCode
##### Redirect www to non-www -- END
END;
}
break;
}
if (!empty($config->olddomain))
{
$htaccess .= "##### Redirect old to new domain -- BEGIN\n";
$domains = trim($config->olddomain);
$domains = explode(',', $domains);
$newHTTPDomain = $config->httphost;
$newHTTPSDomain = $config->httpshost;
foreach ($domains as $olddomain)
{
$olddomain = trim($olddomain);
if (empty($olddomain))
{
continue;
}
$httpRedirect = $olddomain != $newHTTPDomain;
$httpsRedirect = $olddomain != $newHTTPSDomain;
$olddomain = $this->escape_string_for_regex($olddomain);
if ($httpRedirect && !$hasHSTS)
{
$htaccess .= <<<END
## Plain HTTP
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteCond %{HTTP_HOST} ^$olddomain [NC]
RewriteRule (.*) http://$newHTTPDomain/$1 $redirCode
END;
}
if ($httpsRedirect && !$hasHSTS)
{
$htaccess .= <<<END
## HTTPS
RewriteCond %{HTTPS} =on [OR]
RewriteCond %{HTTP:X-Forwarded-Proto} ==https
RewriteCond %{HTTP_HOST} ^$olddomain [NC]
RewriteRule (.*) https://$newHTTPSDomain/$1 $redirCode
END;
}
if ($httpsRedirect && $hasHSTS)
{
$htaccess .= <<<END
## Forced HTTPS - You have enabled the HSTS feature
RewriteCond %{HTTP_HOST} ^$olddomain [NC]
RewriteRule (.*) https://$newHTTPSDomain/$1 $redirCode
END;
}
}
$htaccess .= "##### Redirect old to new domain -- END\n\n";
}
if (!empty($config->httpsurls))
{
$htaccess .= "##### Force HTTPS for certain pages -- BEGIN\n";
foreach ($config->httpsurls as $url)
{
if (empty($url))
{
continue;
}
$urlesc = '^' . $this->escape_string_for_regex($url) . '$';
$htaccess .= <<<END
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteRule $urlesc https://{$config->httpshost}/$url $redirCode
END;
}
$htaccess .= "##### Force HTTPS for certain pages -- END\n\n";
}
$htaccess .= <<<END
##### Rewrite rules to block out some common exploits -- BEGIN
RewriteCond %{QUERY_STRING} proc/self/environ [OR]
RewriteCond %{QUERY_STRING} mosConfig_[a-zA-Z_]{1,21}(=|\%3D) [OR]
RewriteCond %{QUERY_STRING} base64_(en|de)code\(.*\) [OR]
RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
RewriteCond %{QUERY_STRING} GLOBALS(=|\[|\%[0-9A-Z]{0,2}) [OR]
RewriteCond %{QUERY_STRING} _REQUEST(=|\[|\%[0-9A-Z]{0,2})
RewriteRule .* index.php [F]
##### Rewrite rules to block out some common exploits -- END
END;
if ($config->fileinj == 1)
{
$htaccess .= <<<END
##### File injection protection -- BEGIN
RewriteCond %{REQUEST_METHOD} GET
RewriteCond %{QUERY_STRING} [a-zA-Z0-9_]=http[s]?:// [OR]
RewriteCond %{QUERY_STRING} [a-zA-Z0-9_]=(\.\.//?)+ [OR]
RewriteCond %{QUERY_STRING} [a-zA-Z0-9_]=/([a-z0-9_.]//?)+ [NC]
RewriteRule .* - [F]
##### File injection protection -- END
END;
}
// Advanced server protection
if ($config->frontendprot == 1 || $config->backendprot == 1)
{
$htaccess .= "##### Advanced server protection rules exceptions -- BEGIN\n";
if (!empty($config->exceptionfiles))
{
foreach ($config->exceptionfiles as $file)
{
$file = '^' . $this->escape_string_for_regex($file) . '$';
$htaccess .= <<<END
RewriteRule $file - [L]
END;
}
}
if (!empty($config->exceptiondirs))
{
foreach ($config->exceptiondirs as $dir)
{
$dir = trim($dir, '/');
$dir = $this->escape_string_for_regex($dir);
$htaccess .= <<<END
RewriteCond %{REQUEST_FILENAME} !(\.php)$
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^$dir/ - [L]
END;
}
}
if (!empty($config->fullaccessdirs))
{
foreach ($config->fullaccessdirs as $dir)
{
$dir = trim($dir, '/');
$dir = $this->escape_string_for_regex($dir);
$htaccess .= <<<END
RewriteRule ^$dir/ - [L]
END;
}
}
$htaccess .= "##### Advanced server protection rules exceptions -- END\n\n";
}
$htaccess .= "##### Advanced server protection -- BEGIN\n\n";
if ($config->frontendprot == 1 || $config->backendprot == 1)
{
if ($config->backendprot == 1)
{
$directories = $this->bugfixBackendProtectionExclusionDirectories($config->bepexdirs ?: []);
$bedirs = implode('|', $directories);
$betypes = is_array($config->bepextypes) ? $config->bepextypes : explode(',', $config->bepextypes);
$betypes = array_map('trim', $betypes);
$betypes = array_filter($betypes, function ($x) {
return !empty($x);
});
$betypes = implode('|', $betypes);
$htaccess .= <<<END
#### Back-end protection
RewriteRule ^administrator/?$ - [L]
RewriteRule ^administrator/index\.(php|html?)$ - [L]
RewriteRule ^administrator/($bedirs)/.*\.($betypes)$ - [L,NC]
RewriteRule ^administrator/ - [F]
END;
}
if ($config->frontendprot == 1)
{
$fedirs = implode('|', $config->fepexdirs);
$fetypes = is_array($config->fepextypes) ? $config->fepextypes : explode(',', $config->fepextypes);
$fetypes = array_map('trim', $fetypes);
$fetypes = array_filter($fetypes, function ($x) {
return !empty($x);
});
$fetypes = implode('|', $fetypes);
$htaccess .= <<<END
#### Front-end protection
END;
if ($config->backendprot != 1)
{
/**
* When we have frontend protection enabled BUT backend protection disabled, the "Disallow access to all
* other front-end folders" and the "Disallow access to all other front-end files" rules will also block
* access to the administrator directory. Therefore we need to explicitly allow it _before_ we apply the
* front-end protection
*/
$htaccess .= <<< HTACCESS
## Prevent administrator access from being blocked by the front-end protection
RewriteRule ^administrator$ - [L]
RewriteRule ^administrator/ - [L]
HTACCESS;
}
$htaccess .= <<<END
## Allow limited access to additional TinyMCE plugins' HTML files
RewriteRule ^media/plg_editors_tinymce/js/plugins/.*\.(htm|html)$ - [L,NC]
## Allow limited access for certain directories with client-accessible content
RewriteRule ^($fedirs)/.*\.($fetypes)$ - [L,NC]
RewriteRule ^($fedirs)/ - [F]
END;
$htaccess .= <<< END
## Disallow front-end access for certain Joomla! system directories (unless access to their files is allowed above)
RewriteRule ^includes/js/ - [L]
RewriteRule ^(cache|includes|language|logs|log|tmp)/ - [F]
RewriteRule ^(configuration\.php|CONTRIBUTING\.md|htaccess\.txt|joomla\.xml|LICENSE\.txt|phpunit\.xml|README\.txt|web\.config\.txt) - [F]
## Explicitly allow access to the site's index.php main entry point file
RewriteRule ^index.php(/.*){0,1}$ - [L]
## Explicitly allow access to the API application's index.php main entry point file
RewriteRule ^api/index.php(/.*){0,1}$ - [L]
## Explicitly allow access to the site's robots.txt file
RewriteRule ^robots.txt$ - [L]
## Disallow access to all other PHP files throughout the site, unless they are explicitly allowed
RewriteCond %{REQUEST_FILENAME} (\.php)$
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule (.*\.php)$ - [F]
END;
}
}
// Advanced server protection rule exceptions also bypass Disable Client-side Risky Behaviour features
if (($config->bestaticrisks == 1 && $config->backendprot == 1) || ($config->festaticrisks == 1 && $config->frontendprot == 1))
{
if ($config->bestaticrisks == 1 && $config->backendprot == 1)
{
$htaccess .= <<< END
#### Disable client-side risky behavior in backend static content
SetEnvIf Request_URI "^/administrator/($bedirs)/.*\.($betypes)$" disable_risky_behaviour
END;
}
if ($config->festaticrisks == 1 && $config->frontendprot == 1)
{
$htaccess .= <<< END
#### Disable client-side risky behavior in frontend static content
SetEnvIf Request_URI "^/($fedirs)/.*\.($fetypes)$" disable_risky_behaviour
END;
}
$htaccess .= "##### Always allow TinyMCE plugin files to load scripts (they need to)\n";
if ($config->bestaticrisks && $config->backendprot)
{
$htaccess .= "SetEnvIf Request_URI \"^/media/plg_editors_tinymce/js/plugins/.*\.($betypes)$\" !disable_risky_behaviour\n";
}
if ($config->festaticrisks && $config->frontendprot)
{
$htaccess .= "SetEnvIf Request_URI \"^/media/plg_editors_tinymce/js/plugins/.*\.($fetypes)$\" !disable_risky_behaviour\n";
}
$htaccess .= "\n##### Advanced server protection rules exceptions also bypass the “disable client-side risky behavior” features -- BEGIN\n";
foreach ($config->exceptionfiles as $file)
{
$file = $this->escape_string_for_regex(ltrim($file, '/'));
$htaccess .= "SetEnvIf Request_URI \"^/$file$\" !disable_risky_behaviour\n";
}
foreach ($config->exceptiondirs as $dir)
{
$dir = trim($dir, '/');
$dir = $this->escape_string_for_regex($dir);
if ($config->bestaticrisks && $config->backendprot == 1)
{
$htaccess .= "SetEnvIf Request_URI \"^/$dir/.*\.($betypes)$\" !disable_risky_behaviour\n";
}
if ($config->festaticrisks && $config->frontendprot == 1)
{
$htaccess .= "SetEnvIf Request_URI \"^/$dir/.*\.($fetypes)$\" !disable_risky_behaviour\n";
}
}
foreach ($config->fullaccessdirs as $dir)
{
$dir = trim($dir, '/');
$dir = $this->escape_string_for_regex($dir);
if ($config->bestaticrisks && $config->backendprot == 1)
{
$htaccess .= "SetEnvIf Request_URI \"^/$dir/.*\.($betypes)$\" !disable_risky_behaviour\n";
}
if ($config->festaticrisks && $config->frontendprot == 1)
{
$htaccess .= "SetEnvIf Request_URI \"^/$dir/.*\.($fetypes)$\" !disable_risky_behaviour\n";
}
}
$htaccess .= "##### Advanced server protection rules exceptions also bypass the “disable client-side risky behavior” features -- END\n\n";
$htaccess .= <<< HTACCESS
# Apply the "Disable client-side risky behavior" features
Header always set Content-Security-Policy "default-src 'self'; script-src 'none';" env=disable_risky_behaviour
HTACCESS;
}
if ($config->leftovers == 1)
{
$htaccess .= <<<END
## Disallow access to htaccess.txt, php.ini, .user.ini and configuration.php-dist
RewriteRule ^(htaccess\.txt|configuration\.php-dist|php\.ini|\.user\.ini)$ - [F]
END;
}
if ($config->frontendprot == 1)
{
$htaccess .= <<<END
# Disallow access to all other front-end folders
RewriteCond %{REQUEST_FILENAME} -d
RewriteCond %{REQUEST_URI} !^/
RewriteRule .* - [F]
# Disallow access to all other front-end files
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule !^index.php$ - [F]
END;
}
if ($config->clickjacking == 1)
{
$action = version_compare($apacheVersion, '2.0', 'ge') ? 'always set' : 'set';
$htaccess .= <<< ENDCONF
## Protect against clickjacking
<IfModule mod_headers.c>
Header $action X-Frame-Options SAMEORIGIN
# The `X-Frame-Options` response header should be send only for
# HTML documents and not for the other resources.
<FilesMatch "\.(appcache|atom|bbaw|bmp|crx|css|cur|eot|f4[abpv]|flv|geojson|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|topojson|tt[cf]|txt|vcard|vcf|vtt|webapp|web[mp]|woff2?|xloc|xml|xpi)$">
Header unset X-Frame-Options
</FilesMatch>
</IfModule>
ENDCONF;
}
if ($config->reducemimetyperisks == 1)
{
$htaccess .= <<< HTACCESS
## Reduce MIME type security risks
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
</IfModule>
HTACCESS;
}
if ($config->reflectedxss == 1)
{
$htaccess .= <<< HTACCESS
## Reflected XSS prevention
<IfModule mod_headers.c>
Header set X-XSS-Protection "1; mode=block"
</IfModule>
# mod_headers cannot match based on the content-type, however,
# the X-XSS-Protection response header should be sent only for
# HTML documents and not for the other resources.
<IfModule mod_headers.c>
<FilesMatch "\.(appcache|atom|bbaw|bmp|crx|css|cur|eot|f4[abpv]|flv|geojson|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|topojson|tt[cf]|txt|vcard|vcf|vtt|webapp|web[mp]|webmanifest|woff2?|xloc|xml|xpi)$">
Header unset X-XSS-Protection
</FilesMatch>
</IfModule>
HTACCESS;
}
if ($config->svgneutralise)
{
$htaccess .= <<< HTACCESS
## Neutralize scripts in SVG files
<FilesMatch "\.svg$">
<IfModule mod_headers.c>
Header always set Content-Security-Policy "script-src 'none'"
</IfModule>
</FilesMatch>
HTACCESS;
}
if ($config->noserversignature == 1)
{
$htaccess .= <<< HTACCESS
## Remove Apache and PHP version signature
<IfModule mod_headers.c>
Header always unset X-Powered-By
Header always unset X-Content-Powered-By
</IfModule>
ServerSignature Off
HTACCESS;
}
if ($config->notransform == 1)
{
$htaccess .= <<< HTACCESS
## Prevent content transformation
<IfModule mod_headers.c>
Header merge Cache-Control "no-transform"
</IfModule>
HTACCESS;
}
$htaccess .= "##### Advanced server protection -- END\n\n";
if ($config->hstsheader == 1)
{
$action = version_compare($apacheVersion, '2.0', 'ge') ? 'always set' : 'set';
$htaccess .= <<<END
## HSTS Header - See http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
<IfModule mod_headers.c>
SetEnvIfExpr "%{HTTPS}='on'" USE_HSTS_HEADER
SetEnvIf X-Forwarded-Proto "https" USE_HSTS_HEADER
Header $action Strict-Transport-Security "max-age=31536000" env=USE_HSTS_HEADER
</IfModule>
END;
}
elseif ($config->hstsheader == 2)
{
$action = version_compare($apacheVersion, '2.0', 'ge') ? 'always set' : 'set';
$htaccess .= <<<END
## HSTS Header - See http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
<IfModule mod_headers.c>
SetEnvIfExpr "%{HTTPS}='on'" USE_HSTS_HEADER
SetEnvIf X-Forwarded-Proto "https" USE_HSTS_HEADER
Header $action Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" env=USE_HSTS_HEADER
</IfModule>
END;
}
if ($config->notracetrack == 1)
{
/**
* Note to self: using [TraceEnable](https://httpd.apache.org/docs/2.4/mod/core.html#traceenable) will NOT work in
* a .htaccess file as it's only allowed in server and vhost configuration. Using rewrite rules is the only way to
* block TRACE requests in .htaccess.
*/
$tmpRedirCode = $serverCaps->customCodes ? '[R=405,L]' : '[F,L]';
$htaccess .= <<<END
## Disable HTTP methods TRACE and TRACK (protect against XST)
RewriteCond %{REQUEST_METHOD} ^TRACE
RewriteRule ^ - $tmpRedirCode
END;
}
if ($config->cors == 1)
{
$action = version_compare($apacheVersion, '2.0', 'ge') ? 'always set' : 'set';
$htaccess .= <<<END
## Explicitly enable Cross-Origin Resource Sharing (CORS) -- See http://enable-cors.org/
<IfModule mod_headers.c>
Header $action Access-Control-Allow-Origin "*"
Header $action Timing-Allow-Origin "*"
</IfModule>
END;
}
elseif ($config->cors == -1)
{
$action = version_compare($apacheVersion, '2.0', 'ge') ? 'always set' : 'set';
$htaccess .= <<<END
## Explicitly disable Cross-Origin Resource Sharing (CORS) -- See http://enable-cors.org/
<IfModule mod_headers.c>
Header $action Cross-Origin-Resource-Policy "same-origin"
</IfModule>
END;
}
if ($config->referrerpolicy !== '-1')
{
$action = version_compare($apacheVersion, '2.0', 'ge') ? 'always set' : 'set';
$htaccess .= <<<END
## Referrer-policy
<IfModule mod_headers.c>
Header $action Referrer-Policy "{$config->referrerpolicy}"
</IfModule>
END;
}
if ($config->utf8charset == 1)
{
$htaccess .= <<<END
## Set the UTF-8 character set as the default
# Serve all resources labeled as `text/html` or `text/plain`
# with the media type `charset` parameter set to `UTF-8`.
AddDefaultCharset utf-8
# Serve the following file types with the media type `charset`
# parameter set to `UTF-8`.
#
# https://httpd.apache.org/docs/current/mod/mod_mime.html#addcharset
<IfModule mod_mime.c>
AddCharset utf-8 .atom \
.bbaw \
.css \
.geojson \
.js \
.json \
.jsonld \
.rdf \
.rss \
.topojson \
.vtt \
.webapp \
.xloc \
.xml
</IfModule>
END;
}
$htaccess .= <<<END
##### Joomla! core SEF Section -- BEGIN
END;
$htaccess .= <<< APACHE
# -- SEF URLs for the API application
RewriteCond %{REQUEST_URI} ^/api/
RewriteCond %{REQUEST_URI} !^/api/index\.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* api/index.php [L]
# -- SEF URLs for the public frontend application
APACHE;
$htaccess .= <<<END
##### Joomla! core SEF Section -- BEGIN
RewriteCond %{REQUEST_URI} !^/index\.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* index.php [L]
##### Joomla! core SEF Section -- END
END;
$htaccess .= "\n\n" . $config->custfoot . "\n";
return $htaccess;
}
}