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 NginxconfmakerModel extends ServerconfigmakerModel
{
/**
* The current configuration of this feature
*
* @var object
*/
protected $configKey = 'nginxconfig';
/**
* 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 = 'nginx.conf';
/** @inheritdoc */
protected $usePublicRoot = false;
/** @inheritdoc */
public function isSupported(): int
{
return ServerTechnology::isNginxSupported();
}
/**
* Compile and return the contents of the NginX configuration file
*
* @return string
*/
public function makeConfigFile()
{
// Make sure we are called by an expected caller
ServerTechnology::checkCaller($this->allowedCallersForMake);
$app = Factory::getApplication();
$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;
$config = (object) $this->loadConfiguration();
// Load the fastcgi_pass setting
$fastcgi_pass_block = $config->fastcgi_pass_block;
if (empty($fastcgi_pass_block))
{
$fastcgi_pass_block = 'fastcgi_pass 127.0.0.1:9000;';
}
$fastcgi_pass_block = trim($fastcgi_pass_block);
// Get the directory to the site's root
$rewritebase = $config->rewritebase;
$rewritebaseSlash = '/' . trim($rewritebase, '/ ');
$rewritebaseSlash = ($rewritebaseSlash == '/') ? '' : $rewritebaseSlash;
$rewritebase = '/' . trim($rewritebase, '/ ');
$nginxConf = <<<END
### ===========================================================================
### Security Enhanced & Highly Optimized NginX Configuration File for Joomla!
### automatically generated by Admin Tools $version on $d
### ===========================================================================
###
### 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. !!
### !! !!
### !! Remember to include this file in your site's configuration file. !!
### !! Also remember to reload or restart NginX after making any change to !!
### !! this file. !!
### !! !!
### !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
###
### Prevent access to this file
location = $rewritebaseSlash/nginx.conf {
log_not_found off;
access_log off;
return 404;
break;
}
location = $rewritebaseSlash/nginx.conf.admintools {
log_not_found off;
access_log off;
return 404;
break;
}
# Unified HTTP/HTTPS protocol detection into the \$akActualProto variable
set \$akActualProto "http";
if (\$http_x_forwarded_proto = "https") {
set \$akActualProto "https";
}
if (\$scheme = "https") {
set \$akActualProto "https";
}
END;
// 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'));
}
$nginxConf .= <<< END
location $rewritebaseSlash/ {
##### Restricted access by IP address -- BEGIN
END;
foreach ($restrictIPList as $ip)
{
$nginxConf .= ' allow ' . $ip . ";\n";
}
$nginxConf .= <<< END
deny all;
##### Restricted access by IP address -- END
# Joomla public frontend application
try_files \$uri \$uri/ $rewritebaseSlash/index.php?\$args;
}
END;
}
// Protect against common file injection attacks?
if ($config->fileinj == 1)
{
$nginxConf .= <<< CONFDATA
######################################################################
## Protect against common file injection attacks
######################################################################
set \$file_injection 0;
if (\$query_string ~ "[a-zA-Z0-9_]=http://") {
set \$file_injection 1;
}
if (\$query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") {
set \$file_injection 1;
}
if (\$query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") {
set \$file_injection 1;
}
if (\$file_injection = 1) {
return 403;
break;
}
CONFDATA;
}
if ($config->leftovers == 1)
{
$nginxConf .= <<<END
######################################################################
## Block access to configuration.php-dist and htaccess.txt
######################################################################
location = $rewritebaseSlash/configuration.php-dist {
log_not_found off;
access_log off;
return 404;
break;
}
location = $rewritebaseSlash/htaccess.txt {
log_not_found off;
access_log off;
return 404;
break;
}
location = $rewritebaseSlash/web.config {
log_not_found off;
access_log off;
return 404;
break;
}
location = $rewritebaseSlash/configuration.php {
log_not_found off;
access_log off;
return 404;
break;
}
location = $rewritebaseSlash/CONTRIBUTING.md {
log_not_found off;
access_log off;
return 404;
break;
}
location = $rewritebaseSlash/joomla.xml {
log_not_found off;
access_log off;
return 404;
break;
}
location = $rewritebaseSlash/LICENSE.txt {
log_not_found off;
access_log off;
return 404;
break;
}
location = $rewritebaseSlash/phpunit.xml {
log_not_found off;
access_log off;
return 404;
break;
}
location = $rewritebaseSlash/README.txt {
log_not_found off;
access_log off;
return 404;
break;
}
location = $rewritebaseSlash/web.config.txt {
log_not_found off;
access_log off;
return 404;
break;
}
END;
}
if ($config->clickjacking == 1)
{
$nginxConf .= <<< ENDCONF
## Protect against clickjacking
add_header X-Frame-Options SAMEORIGIN;
ENDCONF;
}
if (!empty($config->hoggeragents) && ($config->nohoggers == 1))
{
$nginxConf .= <<< ENDCONF
######################################################################
## Block access from specific user agents
######################################################################
set \$bad_ua 0;
ENDCONF;
foreach ($config->hoggeragents as $agent)
{
$nginxConf .= <<< ENDCONF
if (\$http_user_agent ~ "$agent") {
set \$bad_ua 1;
}
ENDCONF;
}
$nginxConf .= <<< ENDCONF
if (\$bad_ua = 1) {
return 403;
}
ENDCONF;
}
if (($config->fileorder == 1) || ($config->nodirlists == 1))
{
$nginxConf .= <<<ENDCONF
######################################################################
## Directory indices and no automatic directory listings
## Forces index.php to be read before the index.htm(l) files
## Also disables showing files in a directory automatically
######################################################################
index index.php index.html index.htm;
ENDCONF;
}
if ($config->symlinks != 0)
{
$nginxConf .= <<<ENDCONF
######################################################################
## Disable following symlinks
######################################################################
ENDCONF;
switch ($config->symlinks)
{
case 1:
$nginxConf .= "disable_symlinks on;\n";
break;
case 2:
$nginxConf .= "disable_symlinks if_not_owner;\n";
break;
}
}
if ($config->exptime != 0)
{
$expWeek = '1w';
$expWeekText = '1 week';
$expMonth = '1M';
$expMonthText = '1 month';
if ($config->exptime == 2)
{
$expWeek = '1y';
$expWeekText = '1 year';
$expMonth = '1y';
$expMonthText = '1 year';
}
$nginxConf .= <<<ENDCONF
######################################################################
## Set default expiration time
######################################################################
# No caching for specific resource types
location ~* \.(appcache|mf)$ {
access_log off; log_not_found off;
expires epoch;
}
location ~* \.(json|xml)$ {
access_log off; log_not_found off;
expires epoch;
}
# RSS and Atom feeds : 1 hour (hardcoded)
location ~* \.(rss|atom)$ {
access_log off; log_not_found off;
expires 1h;
add_header Cache-Control "public";
}
# CSS and JavaScript : $expWeekText
location ~* \.(css|js|jsonld)$ {
access_log off; log_not_found off;
expires $expWeek;
add_header Cache-Control "public";
}
# Image files : $expMonthText
location ~* \.(bmp|gif|jpg|jpeg|jp2|jfif|png|svg|tif|tiff|ico|wbmp|wbxml|smil|webp)$ {
access_log off; log_not_found off;
expires $expMonth;
add_header Cache-Control "public";
}
# Font files : $expMonthText
location ~* \.(woff|woff2|ttf|otf|eot)$ {
access_log off; log_not_found off;
expires $expMonth;
add_header Cache-Control "public";
}
# Audio files : $expMonthText
location ~* \.(3gp|3g2|aac|mid|midi|mp3|m4a|m4r|aif|aiff|ra|ram|wav|voc|ogg|ogx|opus|aif|aiff|aifc|m3u|m3u8)$ {
access_log off; log_not_found off;
expires $expMonth;
add_header Cache-Control "public";
}
# Video files : $expMonthText
location ~* \.(swf|vrml|avi|mkv|mpg|mpeg|mp4|m4v|mov|asf)$ {
access_log off; log_not_found off;
expires $expMonth;
add_header Cache-Control "public";
}
ENDCONF;
}
if ($config->autocompress == 1)
{
$nginxConf .= <<<ENDCONF
######################################################################
## Automatic compression of static resources
## Compress text, html, javascript, css, xml and other static resources
## May kill access to your site for old versions of Internet Explorer
######################################################################
# The following is the actual automatic compression setup
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_proxied expired no-cache no-store private auth;
gzip_min_length 1000;
gzip_http_version 1.1;
gzip_types text/plain text/css application/xhtml+xml application/xml+rss application/rss+xml application/x-javascript application/javascript text/javascript application/json text/xml application/xml image/svg+xml;
gzip_buffers 16 8k;
gzip_disable "MSIE [1-6]\.(?!.*SV1)";
ENDCONF;
}
if ($config->etagtype != -1)
{
$etagValue = ($config->etagtype == 1) ? 'on' : 'off';
$nginxConf .= <<< CONF
## Send ETag (you have set it to '$etagValue')
etag $etagValue;
CONF;
}
$host = strtolower($config->httphost);
if (substr($host, 0, 4) == 'www.')
{
$wwwHost = $host;
$noWwwHost = substr($host, 4);
}
else
{
$noWwwHost = $host;
$wwwHost = 'www.' . $host;
}
$subfolder = trim($config->rewritebase, '/') ? trim($config->rewritebase, '/') . '/' : '';
switch ($config->wwwredir)
{
case 1:
// non-www to www
$nginxConf .= <<<END
######################################################################
## Redirect non-www to www
######################################################################
if (\$host = '$noWwwHost' ) {
rewrite ^/(.*)$ \$akActualProto://$wwwHost/$subfolder$1 permanent;
}
END;
break;
case 2:
// www to non-www
$nginxConf .= <<<END
######################################################################
## Redirect www to non-www
######################################################################
if (\$host = '$wwwHost' ) {
rewrite ^/(.*)$ \$akActualProto://$noWwwHost/$subfolder$1 permanent;
}
END;
break;
}
if (!empty($config->olddomain))
{
$nginxConf .= <<<END
######################################################################
## Redirect old to new domains
######################################################################
END;
$domains = trim($config->olddomain);
$domains = explode(',', $domains);
$newdomain = $config->httphost;
foreach ($domains as $olddomain)
{
$olddomain = trim($olddomain);
if (empty($olddomain))
{
continue;
}
$olddomain = $this->escape_string_for_regex($olddomain);
$nginxConf .= <<<END
if (\$host ~ "$olddomain$" ) {
rewrite ^/(.*)$ \$akActualProto://$newdomain/$1 permanent;
}
END;
}
}
if ($config->hstsheader == 1)
{
$nginxConf .= <<<END
## HSTS Header - See http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
add_header Strict-Transport-Security "max-age=31536000" always;
END;
}
elseif ($config->hstsheader == 2)
{
$nginxConf .= <<<END
## HSTS Header - See http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
END;
}
if ($config->cors == 1)
{
$nginxConf .= <<<END
## Explicitly enable Cross-Origin Resource Sharing (CORS)
add_header Access-Control-Allow-Origin "*";
add_header Timing-Allow-Origin "*";
END;
}
elseif ($config->cors == -1)
{
$nginxConf .= <<<END
## Explicitly disable Cross-Origin Resource Sharing (CORS)
add_header Cross-Origin-Resource-Policy "same-origin";
END;
}
if ($config->referrerpolicy !== '-1')
{
$nginxConf .= <<<END
## Referrer-policy
add_header Referrer-Policy "{$config->referrerpolicy}";
END;
}
if ($config->notracetrack == 1)
{
$nginxConf .= <<<END
## Disable HTTP methods TRACE and TRACK (protect against XST)
if (\$request_method ~ ^(TRACE|TRACK)$ ) {
return 405;
}
END;
}
if ($config->reducemimetyperisks == 1)
{
$nginxConf .= <<< ENDCONF
## Reduce MIME type security risks
add_header X-Content-Type-Options "nosniff";
ENDCONF;
}
if ($config->reflectedxss == 1)
{
$nginxConf .= <<< ENDCONF
## Reflected XSS prevention
add_header X-XSS-Protection "1; mode=block";
ENDCONF;
}
if ($config->svgneutralise) {
$nginxConf .= <<< ENDCONF
## Neutralize scripts in SVG files
location ~* "\.svg$" {
add_header Content-Security-Policy "script-src 'none'"
}
ENDCONF;
}
if ($config->noserversignature == 1)
{
$nginxConf .= <<< ENDCONF
## Remove NginX and PHP version signature
add_header X-Powered-By "";
add_header X-Content-Powered-By "";
ENDCONF;
}
if ($config->notransform == 1)
{
$nginxConf .= <<< ENDCONF
## Prevent content transformation
add_header Cache-Control "no-transform";
ENDCONF;
}
if ($config->cfipfwd == 1)
{
$trustedSources = '';
if (!empty($config->proxy_ips))
{
$trustedSources = "## User-supplied reverse proxy IP address(es)\n" .
implode("\n", array_map(function ($x) {
return 'set_real_ip_from ' . $x . ';';
}, array_filter($config->proxy_ips, function ($x) {
return !empty($x);
})
)
);
}
$nginxConf .= <<<END
######################################################################
## Real IP forwarding support
######################################################################
$trustedSources
real_ip_header X-Forwarded-For;
END;
}
if ($config->opttimeout == 1)
{
$nginxConf .= <<<END
# -- Timeout handling, see http://wiki.nginx.org/HttpCoreModule
client_header_timeout 10;
client_body_timeout 10;
send_timeout 30;
keepalive_timeout 30s;
END;
}
if ($config->optsockets == 1)
{
$nginxConf .= <<<END
# -- Socket settings, see http://wiki.nginx.org/HttpCoreModule
connection_pool_size 8192;
client_header_buffer_size 4k;
large_client_header_buffers 8 8k;
request_pool_size 8k;
END;
}
if ($config->opttcpperf == 1)
{
$nginxConf .= <<<END
# -- Performance, see http://wiki.nginx.org/HttpCoreModule
sendfile on;
sendfile_max_chunk 1m;
postpone_output 0;
tcp_nopush on;
tcp_nodelay on;
END;
}
if ($config->optoutbuf == 1)
{
$nginxConf .= <<<END
# -- Output buffering, see http://wiki.nginx.org/HttpCoreModule
output_buffers 8 32k;
END;
}
if ($config->optfhndlcache == 1)
{
$nginxConf .= <<<END
# -- Filehandle Cache, useful when serving a large number of static files (Joomla! sites do that)
open_file_cache max=2000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
END;
}
if ($config->encutf8 == 1)
{
$nginxConf .= <<<END
# -- Character encoding, see http://wiki.nginx.org/HttpCharsetModule
charset utf-8;
source_charset utf-8;
END;
}
if ($config->nginxsecurity == 1)
{
$nginxConf .= <<<END
# -- Security options, see http://wiki.nginx.org/HttpCoreModule
server_name_in_redirect off;
server_tokens off;
ignore_invalid_headers on;
END;
}
if ($config->maxclientbody == 1)
{
$nginxConf .= <<<END
# -- Maximum client body size set to 1 Gigabyte
client_max_body_size 1G;
END;
}
if ($config->blockcommon == 1)
{
$nginxConf .= <<<END
set \$common_exploit 0;
if (\$query_string ~ "proc/self/environ") {
set \$common_exploit 1;
}
if (\$query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") {
set \$common_exploit 1;
}
if (\$query_string ~ "base64_(en|de)code\(.*\)") {
set \$common_exploit 1;
}
if (\$query_string ~ "(<|%3C).*script.*(>|%3E)") {
set \$common_exploit 1;
}
if (\$query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") {
set \$common_exploit 1;
}
if (\$query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") {
set \$common_exploit 1;
}
if (\$common_exploit = 1) {
return 403;
}
END;
}
if ($config->enablesef == 1)
{
$nginxConf .= <<<NGINX
## Enable SEF URLs
NGINX;
$nginxConf .= <<<NGINX
# Joomla API application
location $rewritebaseSlash/api/ {
try_files \$uri \$uri/ $rewritebaseSlash/api/index.php?\$args;
}
NGINX;
if ($restrictIP === 'none')
{
$nginxConf .= <<<NGINX
# Joomla public frontend application
location $rewritebaseSlash/ {
try_files \$uri \$uri/ $rewritebaseSlash/index.php?\$args;
}
NGINX;
}
$nginxConf .= <<<NGINX
# Parse index.php as a PHP executable file
location ~* ^$rewritebaseSlash/index.php$ {
$fastcgi_pass_block
break;
}
NGINX;
}
$nginxConf .= <<< END
######################################################################
## Advanced server protection rules exceptions
######################################################################
END;
$hasBackendCsp = ($config->backendprot == 1) && ($config->bestaticrisks == 1);
$hasFrontendCsp = ($config->frontendprot == 1) && ($config->festaticrisks == 1);
$antiCsp = ($hasBackendCsp || $hasFrontendCsp) ? 'add_header Content-Security-Policy "default-src \'self\' \'unsafe-eval\' \'unsafe-inline\';";' : '';
foreach ($config->exceptionfiles as $file)
{
if (substr($file, -4) == '.php')
{
$nginxConf .= <<<END
location = $rewritebaseSlash/$file {
$antiCsp
$fastcgi_pass_block
break;
}
END;
continue;
}
$nginxConf .= <<<END
location = $rewritebaseSlash/$file {
$antiCsp
break;
}
END;
}
foreach ($config->exceptiondirs as $dir)
{
$dir = trim($dir, '/');
$dir = $this->escape_string_for_regex($dir);
$nginxConf .= <<<END
location ~* ^$rewritebaseSlash/$dir/.*\.php$
{
break;
}
location ~* ^$rewritebaseSlash/$dir/.*$
{
$antiCsp
break;
}
END;
}
foreach ($config->fullaccessdirs as $dir)
{
$dir = trim($dir, '/');
$dir = $this->escape_string_for_regex($dir);
$nginxConf .= <<<END
location ~* ^$rewritebaseSlash/$dir/.*$
{
$antiCsp
break;
}
END;
}
$nginxConf .= <<< END
######################################################################
## Advanced server protection
######################################################################
END;
if ($config->backendprot == 1)
{
$directories = $this->bugfixBackendProtectionExclusionDirectories($config->bepexdirs ?: []);
$bedirs = implode('|', $directories);
$betypes = is_array($config->bepextypes) ? implode('|', $config->bepextypes) : $config->bepextypes;
$csp = ($config->bestaticrisks == 1) ? 'add_header Content-Security-Policy "default-src \'self\'; script-src \'none\';";' : '';
$nginxConf .= <<<END
# Allow media files in select back-end directories
location ~* ^$rewritebaseSlash/administrator/($bedirs)/.*.($betypes)$ {
$csp
break;
}
# Allow access to the back-end index.php file
location ~ ^$rewritebaseSlash/administrator/index.php(\?.*)?$ {
$fastcgi_pass_block
break;
}
location ~* ^$rewritebaseSlash/administrator$ {
return 301 $rewritebaseSlash/administrator/index.php?\$args;
}
location ~* ^$rewritebaseSlash/administrator/$ {
return 301 $rewritebaseSlash/administrator/index.php?\$args;
}
# Disable access to everything else.
location ~* ^$rewritebaseSlash/administrator.*$ {
# If it is a file, directory or symlink and I haven't deliberately
# enabled access to it, forbid any access to it!
if (-e \$request_filename) {
return 403;
}
# In any other case, just treat as a SEF URL
try_files \$uri \$uri/ $rewritebaseSlash/administrator/index.php?\$args;
}
END;
}
if ($config->frontendprot == 1)
{
$fedirs = is_array($config->fepexdirs) ? implode('|', $config->fepexdirs) : $config->fepexdirs;
$fetypes = is_array($config->fepextypes) ? implode('|', $config->fepextypes) : $config->fepextypes;
$csp = ($config->festaticrisks == 1) ? 'add_header Content-Security-Policy "default-src \'self\'; script-src \'none\';";' : '';
$nginxConf .= <<<END
# Allow limited access to additional TinyMCE plugins' HTML files
location ~* ^$rewritebaseSlash/media/plg_editors_tinymce/js/plugins/.*\.(htm|html)$ {
$csp
break;
}
# Allow media files in select front-end directories
location ~* ^$rewritebaseSlash/($fedirs)/.*.($fetypes)$ {
$csp
break;
}
## Disallow front-end access for certain Joomla! system directories (unless access to their files is allowed above)
location ~* ^$rewritebaseSlash/includes/js/ {
return 403;
}
location ~* ^$rewritebaseSlash/(cache|includes|language|logs|log|tmp)/ {
return 403;
}
END;
if ($config->enablesef != 1)
{
$nginxConf .= <<<END
# Allow access to the front-end index.php file
location ~* ^$rewritebaseSlash/$ {
return 301 $rewritebaseSlash/index.php?\$args;
}
location ^$rewritebaseSlash/index.php$ {
$fastcgi_pass_block
break;
}
END;
}
$nginxConf .= <<<END
# Allow access to /
location ~* ^$rewritebaseSlash/$ {
return 301 $rewritebaseSlash/index.php?\$args;
}
# Disable access to everything else.
location ~* ^$rewritebaseSlash/.*$ {
# If it is a file, directory or symlink and I haven't deliberately
# enabled access to it, forbid any access to it!
if (-e \$request_filename) {
return 403;
}
# In any other case, just treat as a SEF URL
try_files \$uri \$uri/ $rewritebaseSlash/index.php?\$args;
}
END;
}
$nginxConf .= "##### Advanced server protection -- END\n\n";
return $nginxConf;
}
}