diff --git a/administrator/language/en-GB/en-GB.plg_system_httpheaders.ini b/administrator/language/en-GB/en-GB.plg_system_httpheaders.ini index 62272a295086e..da1f219c9befb 100644 --- a/administrator/language/en-GB/en-GB.plg_system_httpheaders.ini +++ b/administrator/language/en-GB/en-GB.plg_system_httpheaders.ini @@ -25,16 +25,11 @@ PLG_SYSTEM_HTTPHEADERS_HSTS_PRELOAD_NOTE="Important" PLG_SYSTEM_HTTPHEADERS_HSTS_PRELOAD_NOTE_DESC="HSTS means that your domain can no longer be called without HTTPS. Once added to the preload list, this is not easy to undo. Domains can be removed, but it takes months for users to make a change with a browser update.
This option is very important to prevent 'man-in-the-middle attacks', so it should be activated in any case, but only if you are sure that HTTPS is supported for domain and all subdomains in the long run! The value for 'max-age' must be set to 63072000 (2 years) for recording." PLG_SYSTEM_HTTPHEADERS_HSTS_SUBDOMAINS_DESC="HSTS should also be enabled for subdomains usually the subdomain 'www' is taken into account when creating the SSL certificate. If further subdomains are used, please note that they are also provided with a valid SSL certificate." PLG_SYSTEM_HTTPHEADERS_HSTS_SUBDOMAINS="Also for subdomains" -PLG_SYSTEM_HTTPHEADERS_MESSAGE_STATICHEADERS_NOT_WRITTEN_NO_SERVER_CONFIGFILE_FOUND="We couldn't find any active .htaccess or web.config file to apply the rules to. Please first rename the htaccess.txt to .htaccess or web.config.txt to web.config and make sure the configuration files works." -PLG_SYSTEM_HTTPHEADERS_MESSAGE_STATICHEADERS_WRITTEN="All static headers have been written to the server configuration file (%s)." -PLG_SYSTEM_HTTPHEADERS_MESSAGE_STATICHEADERS_NOT_WRITTEN="Your %1$s file is not writable or there was a problem creating the file. You will have to upload the following code by hand. Select the following code and then paste into a new text file. Name this file '%1$s' and upload it to your site root folder.
%2$s
" PLG_SYSTEM_HTTPHEADERS_POSTINSTALL_INTRODUCTION_TITLE="HTTP Security Headers" PLG_SYSTEM_HTTPHEADERS_POSTINSTALL_INTRODUCTION_BODY="

Joomla! comes with a built-in set of tools that help you to handle http security headers. These headers help your browser for example to protect your website from XSS and Clickjacking attacks.

You can find more details in the HTTP Header Management Tutorial in the Joomla! Documentation.

" PLG_SYSTEM_HTTPHEADERS_POSTINSTALL_INTRODUCTION_ACTION="Enable default security headers" ; Please do not translate the following two language strings PLG_SYSTEM_HTTPHEADERS_REFERRERPOLICY="Referrer-Policy" -PLG_SYSTEM_HTTPHEADERS_WRITE_STATIC_HEADERS="Write headers to the configuration file" -PLG_SYSTEM_HTTPHEADERS_WRITE_STATIC_HEADERS_DESC="When enabled the headers (excluding the Content-Security-Policy) will be added to the existing server configuration file as soon as you save this extension. This way the headers will be enforced from the server side. At this time we only support .htaccess (Apache) and web.config (IIS) files." PLG_SYSTEM_HTTPHEADERS_XFRAMEOPTIONS="X-Frame-Options" ; Please do not translate 'HTTP Security Headers' in the following language string PLG_SYSTEM_HTTPHEADERS_XML_DESCRIPTION="This Plugin helps you to set the HTTP Security Headers" diff --git a/plugins/system/httpheaders/httpheaders.php b/plugins/system/httpheaders/httpheaders.php index 9e6756475ec52..a239de1a7ef67 100644 --- a/plugins/system/httpheaders/httpheaders.php +++ b/plugins/system/httpheaders/httpheaders.php @@ -16,7 +16,6 @@ use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Uri\Uri; use Joomla\Database\DatabaseDriver; -use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; use Joomla\Filesystem\File; use Joomla\Registry\Registry; @@ -79,38 +78,6 @@ class PlgSystemHttpHeaders extends CMSPlugin implements SubscriberInterface 'style-src', ]; - /** - * The static header configuration as array - * - * @var array - * @since 4.0.0 - */ - private $staticHeaderConfiguration = []; - - /** - * Defines the Server config file type none - * - * @var string - * @since 4.0.0 - */ - const SERVER_CONFIG_FILE_NONE = ''; - - /** - * Defines the Server config file type htaccess - * - * @var string - * @since 4.0.0 - */ - const SERVER_CONFIG_FILE_HTACCESS = '.htaccess'; - - /** - * Defines the Server config file type web.config - * - * @var string - * @since 4.0.0 - */ - const SERVER_CONFIG_FILE_WEBCONFIG = 'web.config'; - /** * Constructor. * @@ -153,8 +120,7 @@ public function __construct(&$subject, $config) public static function getSubscribedEvents(): array { return [ - 'onAfterInitialise' => 'setHttpHeaders', - 'onExtensionAfterSave' => 'writeStaticHttpHeaders', + 'onAfterInitialise' => 'setHttpHeaders', ]; } @@ -179,80 +145,6 @@ public function setHttpHeaders(): void } } - /** - * On saving this plugin we may want to generate the latest static headers - * - * @param Event $event The Event Object with the passed arguments - * - * @return void - * - * @since 4.0.0 - */ - public function writeStaticHttpHeaders(Event $event): void - { - // Read the arguments from the event object - $context = $event->getArgument('0'); - $table = $event->getArgument('1'); - $isNew = $event->getArgument('2'); - - // When the updated extension is not PLG_SYSTEM_HTTPHEADERS we don't do anything - if ($context !== 'com_plugins.plugin' || $table->element !== $this->_name || $table->folder !== $this->_type) - { - return; - } - - // Get the new params saved by the plugin - $pluginParams = new Registry($table->get('params')); - - // When the option is disabled we don't do anything here. - if (!$pluginParams->get('write_static_headers', 0)) - { - return; - } - - $serverConfigFile = $this->getServerConfigFile(); - - if (!$serverConfigFile) - { - $this->app->enqueueMessage( - Text::_('PLG_SYSTEM_HTTPHEADERS_MESSAGE_STATICHEADERS_NOT_WRITTEN_NO_SERVER_CONFIGFILE_FOUND'), - 'warning' - ); - - return; - } - - // Get the StaticHeaderConfiguration - $this->staticHeaderConfiguration = $this->getStaticHeaderConfiguration($pluginParams); - - // Write the static headers - $result = $this->writeStaticHeaders(); - - if (!$result) - { - // Something did not work tell them that and how to update themself. - $this->app->enqueueMessage( - Text::sprintf( - 'PLG_SYSTEM_HTTPHEADERS_MESSAGE_STATICHEADERS_NOT_WRITTEN', - $serverConfigFile, - $this->getRulesForStaticHeaderConfiguration($serverConfigFile) - ), - 'error' - ); - - return; - } - - // Show messge that everything was done - $this->app->enqueueMessage( - Text::sprintf( - 'PLG_SYSTEM_HTTPHEADERS_MESSAGE_STATICHEADERS_WRITTEN', - $serverConfigFile - ), - 'message' - ); - } - /** * Set the CSP header when enabled * @@ -420,338 +312,25 @@ private function compileAutomaticCspHeaderRules(): array } /** - * Return the server config file constant - * - * @return string Constante pointing to the correct server config file or none - * - * @since 4.0.0 - */ - private function getServerConfigFile(): string - { - if (file_exists($this->getServerConfigFilePath(self::SERVER_CONFIG_FILE_HTACCESS)) - && substr(strtolower($_SERVER['SERVER_SOFTWARE']), 0, 6) === 'apache') - { - return self::SERVER_CONFIG_FILE_HTACCESS; - } - // We are not on an apache so lets just check whether the web.config file exits - if (file_exists($this->getServerConfigFilePath(self::SERVER_CONFIG_FILE_WEBCONFIG))) - { - return self::SERVER_CONFIG_FILE_WEBCONFIG; - } - - return self::SERVER_CONFIG_FILE_NONE; - } - - /** - * Return the path to the server config file we check - * - * @param string $file Constante pointing to the correct server config file or none - * - * @return string Expected path to the requested file; Or false on error - * - * @since 4.0.0 - */ - private function getServerConfigFilePath($file): string - { - return JPATH_ROOT . DIRECTORY_SEPARATOR . $file; - } - - /** - * Return the static Header Configuration based on the server config file - * - * @param string $serverConfigFile Constant holding the server configuration file - * - * @return string Buffer style text of the Header Configuration based on the server config file - * - * @since 4.0.0 - */ - private function getRulesForStaticHeaderConfiguration($serverConfigFile): string - { - if ($serverConfigFile === self::SERVER_CONFIG_FILE_HTACCESS) - { - return $this->getHtaccessRulesForStaticHeaderConfiguration(); - } - - if ($serverConfigFile === self::SERVER_CONFIG_FILE_WEBCONFIG) - { - return $this->getWebConfigRulesForStaticHeaderConfiguration(); - } - - return false; - } - - /** - * Return the static Header Configuration based in the .htaccess format - * - * @return string Buffer style text of the Header Configuration based on the server config file; empty string on error - * - * @since 4.0.0 - */ - private function getHtaccessRulesForStaticHeaderConfiguration(): string - { - $oldHtaccessBuffer = file($this->getServerConfigFilePath(self::SERVER_CONFIG_FILE_HTACCESS), FILE_IGNORE_NEW_LINES); - $newHtaccessBuffer = ''; - - if (!$oldHtaccessBuffer) - { - // `file` couldn't read the htaccess we can't do anything at this point - return ''; - } - - $scriptLines = false; - - foreach ($oldHtaccessBuffer as $id => $line) - { - if ($line === '### MANAGED BY PLG_SYSTEM_HTTPHEADERS DO NOT MANUALLY EDIT! - START ###') - { - $scriptLines = true; - continue; - } - - if ($line === '### MANAGED BY PLG_SYSTEM_HTTPHEADERS DO NOT MANUALLY EDIT! - END ###' - || $line === '##############################################################') - { - $scriptLines = false; - continue; - } - - if ($scriptLines) - { - // When we are between our makers all content should be removed - continue; - } - - $newHtaccessBuffer .= $line . PHP_EOL; - } - - $newHtaccessBuffer .= '##############################################################' . PHP_EOL; - $newHtaccessBuffer .= '### MANAGED BY PLG_SYSTEM_HTTPHEADERS DO NOT MANUALLY EDIT! - START ###' . PHP_EOL; - $newHtaccessBuffer .= '' . PHP_EOL; - - foreach ($this->staticHeaderConfiguration as $headerAndClient => $value) - { - $headerAndClient = explode('#', $headerAndClient); - - if (!in_array(strtolower($headerAndClient[0]), ['content-security-policy', 'content-security-policy-report-only'])) - { - $newHtaccessBuffer .= ' Header set ' . $headerAndClient[0] . ' "' . $value . '"' . PHP_EOL; - } - } - - $newHtaccessBuffer .= '' . PHP_EOL; - $newHtaccessBuffer .= '### MANAGED BY PLG_SYSTEM_HTTPHEADERS DO NOT MANUALLY EDIT! - END ###' . PHP_EOL; - $newHtaccessBuffer .= '##############################################################' . PHP_EOL; - $newHtaccessBuffer .= PHP_EOL; - - return $newHtaccessBuffer; - } - - /** - * Return the static Header Configuration based in the web.config format - * - * @return string Buffer style text of the Header Configuration based on the server config file or false on error. - * - * @since 4.0.0 - */ - private function getWebConfigRulesForStaticHeaderConfiguration(): string - { - $webConfigDomDoc = new DOMDocument('1.0', 'UTF-8'); - - // We want a nice output - $webConfigDomDoc->formatOutput = true; - $webConfigDomDoc->preserveWhiteSpace = false; - - // Load the current file into our object - $webConfigDomDoc->load($this->getServerConfigFilePath(self::SERVER_CONFIG_FILE_WEBCONFIG)); - - // Get an DOMXPath Object mathching our file - $xpath = new DOMXPath($webConfigDomDoc); - - // We require an correct tree containing an system.webServer node! - $systemWebServer = $xpath->query("/configuration/location/system.webServer"); - - if ($systemWebServer->length === 0 || $systemWebServer->length > 1) - { - // There is only one (or none) so we don't do anything here - return ''; - } - - // Check what configurations exists already - $httpProtocol = $xpath->query("/configuration/location/system.webServer/httpProtocol"); - $customHeaders = $xpath->query("/configuration/location/system.webServer/httpProtocol/customHeaders"); - - // Does the httpProtocol node exist? - if ($httpProtocol->length === 0) - { - $newHttpProtocol = $webConfigDomDoc->createElement('httpProtocol'); - $newCustomHeaders = $webConfigDomDoc->createElement('customHeaders'); - - foreach ($this->staticHeaderConfiguration as $headerAndClient => $value) - { - $headerAndClient = explode('#', $headerAndClient); - - if (!in_array(strtolower($headerAndClient[0]), ['content-security-policy', 'content-security-policy-report-only'])) - { - $newHeader = $webConfigDomDoc->createElement('add'); - - $newHeader->setAttribute('name', $headerAndClient[0]); - $newHeader->setAttribute('value', $value); - $newCustomHeaders->appendChild($newHeader); - } - } - - $newHttpProtocol->appendChild($newCustomHeaders); - $systemWebServer[0]->appendChild($newHttpProtocol); - } - // It seams there are a httpProtocol node so does the customHeaders node exist? - elseif ($customHeaders->length === 0) - { - $newCustomHeaders = $webConfigDomDoc->createElement('customHeaders'); - - foreach ($this->staticHeaderConfiguration as $headerAndClient => $value) - { - $headerAndClient = explode('#', $headerAndClient); - - if (!in_array(strtolower($headerAndClient[0]), ['content-security-policy', 'content-security-policy-report-only'])) - { - $newHeader = $webConfigDomDoc->createElement('add'); - - $newHeader->setAttribute('name', $headerAndClient[0]); - $newHeader->setAttribute('value', $value); - $newCustomHeaders->appendChild($newHeader); - } - } - - $httpProtocol[0]->appendChild($newCustomHeaders); - } - // Well It seams httpProtocol and customHeaders exists lets check now the individual header (add) nodes - else - { - $oldCustomHeaders = $xpath->query("/configuration/location/system.webServer/httpProtocol/customHeaders/add"); - - // Here we check all headers actually exists with the correct value - foreach ($this->staticHeaderConfiguration as $headerAndClient => $value) - { - $headerAndClient = explode('#', $headerAndClient); - - // When no headers exitsts at all we can't find anything :D - if ($oldCustomHeaders->length === 0) - { - $found = false; - } - - // Check if the header is currently set or not - foreach ($oldCustomHeaders as $oldCustomHeader) - { - $found = false; - $customHeadersName = $oldCustomHeader->getAttribute('name'); - - if ($headerAndClient[0] === $customHeadersName) - { - // We found it, well done. - $found = true; - break; - } - } - - // The header wasn't found we need to create it - if (!$found) - { - if (!in_array(strtolower($headerAndClient[0]), ['content-security-policy', 'content-security-policy-report-only'])) - { - // Generate the new header Element - $newHeader = $webConfigDomDoc->createElement('add'); - $newHeader->setAttribute('name', $headerAndClient[0]); - $newHeader->setAttribute('value', $value); - - // Append the new header - $customHeaders[0]->appendChild($newHeader); - } - } - - $customHeadersValue = $oldCustomHeader->getAttribute('value'); - - if ($value === $customHeadersValue) - { - continue; - } - - $oldCustomHeader->setAttribute('value', $value); - } - } - - return $webConfigDomDoc->saveXML(); - } - - /** - * Wirte the static headers. - * - * @return boolean True on success; false on any error - * - * @since 4.0.0 - */ - private function writeStaticHeaders(): bool - { - $pathToHtaccess = $this->getServerConfigFilePath(self::SERVER_CONFIG_FILE_HTACCESS); - $pathToWebConfig = $this->getServerConfigFilePath(self::SERVER_CONFIG_FILE_WEBCONFIG); - - if (file_exists($pathToHtaccess)) - { - $htaccessContent = $this->getHtaccessRulesForStaticHeaderConfiguration(); - - if (is_readable($pathToHtaccess) && !empty($htaccessContent)) - { - // Write the htaccess using the Frameworks File Class - return File::write($pathToHtaccess, $htaccessContent); - } - } - - if (file_exists($pathToWebConfig)) - { - $webConfigContent = $this->getWebConfigRulesForStaticHeaderConfiguration(); - - if (is_readable($pathToWebConfig) && !empty($webConfigContent)) - { - // Setup and than write the web.config write using DOMDocument - $webConfigDomDoc = new DOMDocument; - $webConfigDomDoc->formatOutput = true; - $webConfigDomDoc->preserveWhiteSpace = false; - $webConfigDomDoc->loadXML($webConfigContent); - - // When the return code is an integer we got the bytes and everything went well if not something broke.. - return is_integer($webConfigDomDoc->save($pathToWebConfig)) ? true : false; - } - } - } - - /** * Get the configured static headers. * - * @param Registry $pluginParams An Registry Object containing the plugin parameters - * * @return array We return the array of static headers with its values. * * @since 4.0.0 */ - private function getStaticHeaderConfiguration($pluginParams = false): array + private function getStaticHeaderConfiguration(): array { $staticHeaderConfiguration = []; - // Fallback to $this->params when no params has been passed - if ($pluginParams === false) - { - $pluginParams = $this->params; - } - // X-frame-options - if ($pluginParams->get('xframeoptions')) + if ($this->params->get('xframeoptions')) { $staticHeaderConfiguration['x-frame-options#both'] = 'SAMEORIGIN'; } // Referrer-policy - $referrerPolicy = (string) $pluginParams->get('referrerpolicy', 'no-referrer-when-downgrade'); + $referrerPolicy = (string) $this->params->get('referrerpolicy', 'no-referrer-when-downgrade'); if ($referrerPolicy !== 'disabled') { @@ -759,20 +338,20 @@ private function getStaticHeaderConfiguration($pluginParams = false): array } // Generate the strict-transport-security header - $strictTransportSecurity = (int) $pluginParams->get('hsts', 0); + $strictTransportSecurity = (int) $this->params->get('hsts', 0); if ($strictTransportSecurity) { - $maxAge = (int) $pluginParams->get('hsts_maxage', 31536000); + $maxAge = (int) $this->params->get('hsts_maxage', 31536000); $hstsOptions = []; $hstsOptions[] = $maxAge < 300 ? 'max-age=300' : 'max-age=' . $maxAge; - if ($pluginParams->get('hsts_subdomains', 0)) + if ($this->params->get('hsts_subdomains', 0)) { $hstsOptions[] = 'includeSubDomains'; } - if ($pluginParams->get('hsts_preload', 0)) + if ($this->params->get('hsts_preload', 0)) { $hstsOptions[] = 'preload'; } @@ -781,7 +360,7 @@ private function getStaticHeaderConfiguration($pluginParams = false): array } // Generate the additional headers - $additionalHttpHeaders = $pluginParams->get('additional_httpheader', []); + $additionalHttpHeaders = $this->params->get('additional_httpheader', []); foreach ($additionalHttpHeaders as $additionalHttpHeader) { @@ -816,14 +395,14 @@ private function getStaticHeaderConfiguration($pluginParams = false): array */ private function setStaticHeaders(): void { - $this->staticHeaderConfiguration = $this->getStaticHeaderConfiguration($this->params); + $staticHeaderConfiguration = $this->getStaticHeaderConfiguration(); - if (empty($this->staticHeaderConfiguration)) + if (empty($staticHeaderConfiguration)) { return; } - foreach ($this->staticHeaderConfiguration as $headerAndClient => $value) + foreach ($staticHeaderConfiguration as $headerAndClient => $value) { $headerAndClient = explode('#', $headerAndClient); $header = $headerAndClient[0]; diff --git a/plugins/system/httpheaders/httpheaders.xml b/plugins/system/httpheaders/httpheaders.xml index b94d7a3337355..2af5fbaa9b2da 100644 --- a/plugins/system/httpheaders/httpheaders.xml +++ b/plugins/system/httpheaders/httpheaders.xml @@ -16,17 +16,6 @@
- - - -