first commit
This commit is contained in:
328
plugins/system/httpheaders/httpheaders.xml
Normal file
328
plugins/system/httpheaders/httpheaders.xml
Normal file
@ -0,0 +1,328 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>plg_system_httpheaders</name>
|
||||
<author>Joomla! Project</author>
|
||||
<creationDate>2017-10</creationDate>
|
||||
<copyright>(C) 2018 Open Source Matters, Inc.</copyright>
|
||||
<license>GNU General Public License version 2 or later; see LICENSE.txt</license>
|
||||
<authorEmail>admin@joomla.org</authorEmail>
|
||||
<authorUrl>www.joomla.org</authorUrl>
|
||||
<version>4.0.0</version>
|
||||
<description>PLG_SYSTEM_HTTPHEADERS_XML_DESCRIPTION</description>
|
||||
<namespace path="src">Joomla\Plugin\System\Httpheaders</namespace>
|
||||
<files>
|
||||
<folder>postinstall</folder>
|
||||
<folder plugin="httpheaders">services</folder>
|
||||
<folder>src</folder>
|
||||
</files>
|
||||
<config>
|
||||
<fields name="params">
|
||||
<fieldset name="basic">
|
||||
<field
|
||||
name="xframeoptions"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_XFRAMEOPTIONS"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="1"
|
||||
filter="integer"
|
||||
validate="options"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="referrerpolicy"
|
||||
type="list"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_REFERRERPOLICY"
|
||||
default="strict-origin-when-cross-origin"
|
||||
validate="options"
|
||||
>
|
||||
<option value="disabled">JDISABLED</option>
|
||||
<option value="no-referrer">no-referrer</option>
|
||||
<option value="no-referrer-when-downgrade">no-referrer-when-downgrade</option>
|
||||
<option value="origin">origin</option>
|
||||
<option value="origin-when-cross-origin">origin-when-cross-origin</option>
|
||||
<option value="same-origin">same-origin</option>
|
||||
<option value="strict-origin">strict-origin</option>
|
||||
<option value="strict-origin-when-cross-origin">strict-origin-when-cross-origin</option>
|
||||
<option value="unsafe-url">unsafe-url</option>
|
||||
</field>
|
||||
<field
|
||||
name="coop"
|
||||
type="list"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_COOP"
|
||||
default="same-origin"
|
||||
validate="options"
|
||||
>
|
||||
<option value="disabled">JDISABLED</option>
|
||||
<option value="same-origin">same-origin</option>
|
||||
<option value="same-origin-allow-popups">same-origin-allow-popups</option>
|
||||
<option value="unsafe-none">unsafe-none</option>
|
||||
</field>
|
||||
<field
|
||||
name="additional_httpheader"
|
||||
type="subform"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_ADDITIONAL_HEADER"
|
||||
multiple="true"
|
||||
>
|
||||
<form>
|
||||
<field
|
||||
name="key"
|
||||
type="list"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_ADDITIONAL_HEADER_KEY"
|
||||
validate="options"
|
||||
class="col-md-4"
|
||||
>
|
||||
<option value="content-security-policy">Content-Security-Policy</option>
|
||||
<option value="content-security-policy-report-only">Content-Security-Policy-Report-Only</option>
|
||||
<option value="cross-origin-opener-policy">Cross-Origin-Opener-Policy</option>
|
||||
<option value="expect-ct">Expect-CT</option>
|
||||
<option value="feature-policy">Feature-Policy</option>
|
||||
<option value="nel">NEL</option>
|
||||
<option value="permissions-policy">Permissions-Policy</option>
|
||||
<option value="referrer-policy">Referrer-Policy</option>
|
||||
<option value="report-to">Report-To</option>
|
||||
<option value="strict-transport-security">Strict-Transport-Security</option>
|
||||
<option value="x-frame-options">X-Frame-Options</option>
|
||||
</field>
|
||||
<field
|
||||
name="value"
|
||||
type="text"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_ADDITIONAL_HEADER_VALUE"
|
||||
class="col-md-10"
|
||||
/>
|
||||
<field
|
||||
name="client"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_HEADER_CLIENT"
|
||||
default="site"
|
||||
validate="options"
|
||||
class="col-md-12"
|
||||
>
|
||||
<option value="site">JSITE</option>
|
||||
<option value="administrator">JADMINISTRATOR</option>
|
||||
<option value="both">PLG_SYSTEM_HTTPHEADERS_HEADER_CLIENT_BOTH</option>
|
||||
</field>
|
||||
</form>
|
||||
</field>
|
||||
</fieldset>
|
||||
<fieldset name="hsts" label="Strict-Transport-Security (HSTS)">
|
||||
<field
|
||||
name="hsts"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_HSTS"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
filter="integer"
|
||||
validate="options"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="hsts_maxage"
|
||||
type="number"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_HSTS_MAXAGE"
|
||||
description="PLG_SYSTEM_HTTPHEADERS_HSTS_MAXAGE_DESC"
|
||||
default="31536000"
|
||||
filter="integer"
|
||||
validate="number"
|
||||
min="300"
|
||||
showon="hsts:1"
|
||||
/>
|
||||
<field
|
||||
name="hsts_subdomains"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_HSTS_SUBDOMAINS"
|
||||
description="PLG_SYSTEM_HTTPHEADERS_HSTS_SUBDOMAINS_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
filter="integer"
|
||||
validate="options"
|
||||
showon="hsts:1"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="hsts_preload"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_HSTS_PRELOAD"
|
||||
description="PLG_SYSTEM_HTTPHEADERS_HSTS_PRELOAD_NOTE_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
filter="integer"
|
||||
validate="options"
|
||||
showon="hsts:1"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
<fieldset name="csp" label="Content-Security-Policy (CSP)">
|
||||
<field
|
||||
name="contentsecuritypolicy"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="contentsecuritypolicy_client"
|
||||
type="list"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_CLIENT"
|
||||
default="site"
|
||||
validate="options"
|
||||
showon="contentsecuritypolicy:1"
|
||||
>
|
||||
<option value="site">JSITE</option>
|
||||
<option value="administrator">JADMINISTRATOR</option>
|
||||
<option value="both">PLG_SYSTEM_HTTPHEADERS_HEADER_CLIENT_BOTH</option>
|
||||
</field>
|
||||
<field
|
||||
name="contentsecuritypolicy_report_only"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_REPORT_ONLY"
|
||||
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_REPORT_ONLY_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="1"
|
||||
showon="contentsecuritypolicy:1"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="nonce_enabled"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_NONCE_ENABLED"
|
||||
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_NONCE_ENABLED_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
showon="contentsecuritypolicy:1"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="script_hashes_enabled"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_SCRIPT_HASHES_ENABLED"
|
||||
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_SCRIPT_HASHES_ENABLED_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
showon="contentsecuritypolicy:1"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="strict_dynamic_enabled"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_STRICT_DYNAMIC_ENABLED"
|
||||
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_STRICT_DYNAMIC_ENABLED_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
showon="contentsecuritypolicy:1"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="style_hashes_enabled"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_STYLE_HASHES_ENABLED"
|
||||
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_STYLE_HASHES_ENABLED_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="0"
|
||||
showon="contentsecuritypolicy:1"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="frame_ancestors_self_enabled"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_FRAME_ANCESTORS_SELF_ENABLED"
|
||||
description="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_FRAME_ANCESTORS_SELF_ENABLED_DESC"
|
||||
layout="joomla.form.field.radio.switcher"
|
||||
default="1"
|
||||
showon="contentsecuritypolicy:1"
|
||||
>
|
||||
<option value="0">JDISABLED</option>
|
||||
<option value="1">JENABLED</option>
|
||||
</field>
|
||||
<field
|
||||
name="contentsecuritypolicy_values"
|
||||
type="subform"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_VALUES"
|
||||
multiple="true"
|
||||
showon="contentsecuritypolicy:1"
|
||||
>
|
||||
<form>
|
||||
<field
|
||||
name="directive"
|
||||
type="list"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_VALUES_DIRECTIVE"
|
||||
class="col-md-4"
|
||||
validate="options"
|
||||
>
|
||||
<option value="child-src">child-src</option>
|
||||
<option value="connect-src">connect-src</option>
|
||||
<option value="default-src">default-src</option>
|
||||
<option value="font-src">font-src</option>
|
||||
<option value="frame-src">frame-src</option>
|
||||
<option value="img-src">img-src</option>
|
||||
<option value="manifest-src">manifest-src</option>
|
||||
<option value="media-src">media-src</option>
|
||||
<option value="prefetch-src">prefetch-src</option>
|
||||
<option value="object-src">object-src</option>
|
||||
<option value="script-src">script-src</option>
|
||||
<option value="script-src-elem">script-src-elem</option>
|
||||
<option value="script-src-attr">script-src-attr</option>
|
||||
<option value="style-src">style-src</option>
|
||||
<option value="style-src-elem">style-src-elem</option>
|
||||
<option value="style-src-attr">style-src-attr</option>
|
||||
<option value="worker-src">worker-src</option>
|
||||
<option value="base-uri">base-uri</option>
|
||||
<option value="plugin-types">plugin-types</option>
|
||||
<option value="sandbox">sandbox</option>
|
||||
<option value="form-action">form-action</option>
|
||||
<option value="frame-ancestors">frame-ancestors</option>
|
||||
<option value="navigate-to">navigate-to</option>
|
||||
<option value="report-uri">report-uri</option>
|
||||
<option value="report-to">report-to</option>
|
||||
<option value="block-all-mixed-content">block-all-mixed-content</option>
|
||||
<option value="upgrade-insecure-requests">upgrade-insecure-requests</option>
|
||||
<option value="require-sri-for">require-sri-for</option>
|
||||
</field>
|
||||
<field
|
||||
name="value"
|
||||
type="text"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_CONTENTSECURITYPOLICY_VALUES_VALUE"
|
||||
class="col-md-10"
|
||||
showon="directive!:block-all-mixed-content[AND]directive!:upgrade-insecure-requests"
|
||||
/>
|
||||
<field
|
||||
name="client"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_HTTPHEADERS_HEADER_CLIENT"
|
||||
default="site"
|
||||
class="col-md-12"
|
||||
>
|
||||
<option value="site">JSITE</option>
|
||||
<option value="administrator">JADMINISTRATOR</option>
|
||||
<option value="both">PLG_SYSTEM_HTTPHEADERS_HEADER_CLIENT_BOTH</option>
|
||||
</field>
|
||||
</form>
|
||||
</field>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
<languages>
|
||||
<language tag="en-GB">language/en-GB/plg_system_httpheaders.ini</language>
|
||||
<language tag="en-GB">language/en-GB/plg_system_httpheaders.sys.ini</language>
|
||||
</languages>
|
||||
</extension>
|
||||
62
plugins/system/httpheaders/postinstall/introduction.php
Normal file
62
plugins/system/httpheaders/postinstall/introduction.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Plugin
|
||||
* @subpackage System.HttpHeaders
|
||||
*
|
||||
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Checks if the plugin is enabled. If not it returns true, meaning that the
|
||||
* message concerning the HTTPHeaders Plugin should be displayed.
|
||||
*
|
||||
* @return integer
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
function httpheaders_postinstall_condition()
|
||||
{
|
||||
return !Joomla\CMS\Plugin\PluginHelper::isEnabled('system', 'httpheaders');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the HTTPHeaders plugin
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
function httpheaders_postinstall_action()
|
||||
{
|
||||
// Enable the plugin
|
||||
$db = Factory::getDbo();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('httpheaders'));
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('extension_id')
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('httpheaders'));
|
||||
$db->setQuery($query);
|
||||
$extensionId = $db->loadResult();
|
||||
|
||||
$url = 'index.php?option=com_plugins&task=plugin.edit&extension_id=' . $extensionId;
|
||||
Factory::getApplication()->redirect($url);
|
||||
}
|
||||
48
plugins/system/httpheaders/services/provider.php
Normal file
48
plugins/system/httpheaders/services/provider.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Plugin
|
||||
* @subpackage System.httpheaders
|
||||
*
|
||||
* @copyright (C) 2023 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
\defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\PluginInterface;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Joomla\Plugin\System\Httpheaders\Extension\Httpheaders;
|
||||
|
||||
return new class () implements ServiceProviderInterface {
|
||||
/**
|
||||
* Registers the service provider with a DI container.
|
||||
*
|
||||
* @param Container $container The DI container.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.4.0
|
||||
*/
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->set(
|
||||
PluginInterface::class,
|
||||
function (Container $container) {
|
||||
$plugin = new Httpheaders(
|
||||
$container->get(DispatcherInterface::class),
|
||||
(array) PluginHelper::getPlugin('system', 'httpheaders'),
|
||||
Factory::getApplication()
|
||||
);
|
||||
$plugin->setDatabase($container->get(DatabaseInterface::class));
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
456
plugins/system/httpheaders/src/Extension/Httpheaders.php
Normal file
456
plugins/system/httpheaders/src/Extension/Httpheaders.php
Normal file
@ -0,0 +1,456 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Joomla.Plugin
|
||||
* @subpackage System.httpheaders
|
||||
*
|
||||
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
namespace Joomla\Plugin\System\Httpheaders\Extension;
|
||||
|
||||
use Joomla\CMS\Application\CMSApplicationInterface;
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\Database\DatabaseAwareTrait;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Joomla\Event\Event;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
|
||||
// phpcs:disable PSR1.Files.SideEffects
|
||||
\defined('_JEXEC') or die;
|
||||
// phpcs:enable PSR1.Files.SideEffects
|
||||
|
||||
/**
|
||||
* Plugin class for HTTP Headers
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
final class Httpheaders extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
use DatabaseAwareTrait;
|
||||
|
||||
/**
|
||||
* The generated csp nonce value
|
||||
*
|
||||
* @var string
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private $cspNonce;
|
||||
|
||||
/**
|
||||
* The list of the supported HTTP headers
|
||||
*
|
||||
* @var array
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private $supportedHttpHeaders = [
|
||||
'strict-transport-security',
|
||||
'content-security-policy',
|
||||
'content-security-policy-report-only',
|
||||
'x-frame-options',
|
||||
'referrer-policy',
|
||||
'expect-ct',
|
||||
'feature-policy',
|
||||
'cross-origin-opener-policy',
|
||||
'report-to',
|
||||
'permissions-policy',
|
||||
'nel',
|
||||
];
|
||||
|
||||
/**
|
||||
* The list of valid directives based on: https://www.w3.org/TR/CSP3/#csp-directives
|
||||
*
|
||||
* @var array
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private $validDirectives = [
|
||||
'child-src',
|
||||
'connect-src',
|
||||
'default-src',
|
||||
'font-src',
|
||||
'frame-src',
|
||||
'img-src',
|
||||
'manifest-src',
|
||||
'media-src',
|
||||
'prefetch-src',
|
||||
'object-src',
|
||||
'script-src',
|
||||
'script-src-elem',
|
||||
'script-src-attr',
|
||||
'style-src',
|
||||
'style-src-elem',
|
||||
'style-src-attr',
|
||||
'worker-src',
|
||||
'base-uri',
|
||||
'plugin-types',
|
||||
'sandbox',
|
||||
'form-action',
|
||||
'frame-ancestors',
|
||||
'navigate-to',
|
||||
'report-uri',
|
||||
'report-to',
|
||||
'block-all-mixed-content',
|
||||
'upgrade-insecure-requests',
|
||||
'require-sri-for',
|
||||
];
|
||||
|
||||
/**
|
||||
* The list of directives without a value
|
||||
*
|
||||
* @var array
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private $noValueDirectives = [
|
||||
'block-all-mixed-content',
|
||||
'upgrade-insecure-requests',
|
||||
];
|
||||
|
||||
/**
|
||||
* The list of directives supporting nonce
|
||||
*
|
||||
* @var array
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private $nonceDirectives = [
|
||||
'script-src',
|
||||
'style-src',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param DispatcherInterface $dispatcher The object to observe -- event dispatcher.
|
||||
* @param array $config An optional associative array of configuration settings.
|
||||
* @param CMSApplicationInterface $app The app
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function __construct(DispatcherInterface $dispatcher, array $config, CMSApplicationInterface $app)
|
||||
{
|
||||
parent::__construct($dispatcher, $config);
|
||||
|
||||
$this->setApplication($app);
|
||||
|
||||
$nonceEnabled = (int) $this->params->get('nonce_enabled', 0);
|
||||
|
||||
// Nonce generation when it's enabled
|
||||
if ($nonceEnabled) {
|
||||
$this->cspNonce = base64_encode(bin2hex(random_bytes(64)));
|
||||
}
|
||||
|
||||
// Set the nonce, when not set we set it to NULL which is checked down the line
|
||||
$this->getApplication()->set('csp_nonce', $this->cspNonce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of events this subscriber will listen to.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'onAfterInitialise' => 'setHttpHeaders',
|
||||
'onAfterRender' => 'applyHashesToCspRule',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The `applyHashesToCspRule` method makes sure the csp hashes are added to the csp header when enabled
|
||||
*
|
||||
* @param Event $event
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function applyHashesToCspRule(Event $event): void
|
||||
{
|
||||
// CSP is only relevant on html pages. Let's early exit here.
|
||||
if ($this->getApplication()->getDocument()->getType() !== 'html') {
|
||||
return;
|
||||
}
|
||||
|
||||
$scriptHashesEnabled = (int) $this->params->get('script_hashes_enabled', 0);
|
||||
$styleHashesEnabled = (int) $this->params->get('style_hashes_enabled', 0);
|
||||
|
||||
// Early exit when both options are disabled
|
||||
if (!$scriptHashesEnabled && !$styleHashesEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$headData = $this->getApplication()->getDocument()->getHeadData();
|
||||
$scriptHashes = [];
|
||||
$styleHashes = [];
|
||||
|
||||
if ($scriptHashesEnabled) {
|
||||
// Generate the hashes for the script-src
|
||||
$inlineScripts = \is_array($headData['script']) ? $headData['script'] : [];
|
||||
|
||||
foreach ($inlineScripts as $type => $scripts) {
|
||||
foreach ($scripts as $hash => $scriptContent) {
|
||||
$scriptHashes[] = "'sha256-" . base64_encode(hash('sha256', $scriptContent, true)) . "'";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($styleHashesEnabled) {
|
||||
// Generate the hashes for the style-src
|
||||
$inlineStyles = \is_array($headData['style']) ? $headData['style'] : [];
|
||||
|
||||
foreach ($inlineStyles as $type => $styles) {
|
||||
foreach ($styles as $hash => $styleContent) {
|
||||
$styleHashes[] = "'sha256-" . base64_encode(hash('sha256', $styleContent, true)) . "'";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the hashes in the csp header when set.
|
||||
$headers = $this->getApplication()->getHeaders();
|
||||
|
||||
foreach ($headers as $id => $headerConfiguration) {
|
||||
if (
|
||||
strtolower($headerConfiguration['name']) === 'content-security-policy'
|
||||
|| strtolower($headerConfiguration['name']) === 'content-security-policy-report-only'
|
||||
) {
|
||||
$newHeaderValue = $headerConfiguration['value'];
|
||||
|
||||
if (!empty($scriptHashes)) {
|
||||
$newHeaderValue = str_replace('{script-hashes}', implode(' ', $scriptHashes), $newHeaderValue);
|
||||
} else {
|
||||
$newHeaderValue = str_replace('{script-hashes}', '', $newHeaderValue);
|
||||
}
|
||||
|
||||
if (!empty($styleHashes)) {
|
||||
$newHeaderValue = str_replace('{style-hashes}', implode(' ', $styleHashes), $newHeaderValue);
|
||||
} else {
|
||||
$newHeaderValue = str_replace('{style-hashes}', '', $newHeaderValue);
|
||||
}
|
||||
|
||||
$this->getApplication()->setHeader($headerConfiguration['name'], $newHeaderValue, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `setHttpHeaders` method handle the setting of the configured HTTP Headers
|
||||
*
|
||||
* @param Event $event
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public function setHttpHeaders(Event $event): void
|
||||
{
|
||||
// Set the default header when they are enabled
|
||||
$this->setStaticHeaders();
|
||||
|
||||
// Handle CSP Header configuration
|
||||
$cspEnabled = (int) $this->params->get('contentsecuritypolicy', 0);
|
||||
$cspClient = (string) $this->params->get('contentsecuritypolicy_client', 'site');
|
||||
|
||||
// Check whether CSP is enabled and enabled by the current client
|
||||
if ($cspEnabled && ($this->getApplication()->isClient($cspClient) || $cspClient === 'both')) {
|
||||
$this->setCspHeader();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the CSP header when enabled
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private function setCspHeader(): void
|
||||
{
|
||||
$cspReadOnly = (int) $this->params->get('contentsecuritypolicy_report_only', 1);
|
||||
$cspHeader = $cspReadOnly === 0 ? 'content-security-policy' : 'content-security-policy-report-only';
|
||||
|
||||
// In custom mode we compile the header from the values configured
|
||||
$cspValues = $this->params->get('contentsecuritypolicy_values', []);
|
||||
$nonceEnabled = (int) $this->params->get('nonce_enabled', 0);
|
||||
$scriptHashesEnabled = (int) $this->params->get('script_hashes_enabled', 0);
|
||||
$strictDynamicEnabled = (int) $this->params->get('strict_dynamic_enabled', 0);
|
||||
$styleHashesEnabled = (int) $this->params->get('style_hashes_enabled', 0);
|
||||
$frameAncestorsSelfEnabled = (int) $this->params->get('frame_ancestors_self_enabled', 1);
|
||||
$frameAncestorsSet = false;
|
||||
|
||||
foreach ($cspValues as $cspValue) {
|
||||
// Handle the client settings foreach header
|
||||
if (!$this->getApplication()->isClient($cspValue->client) && $cspValue->client != 'both') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle non value directives
|
||||
if (\in_array($cspValue->directive, $this->noValueDirectives)) {
|
||||
$newCspValues[] = trim($cspValue->directive);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// We can only use this if this is a valid entry
|
||||
if (
|
||||
\in_array($cspValue->directive, $this->validDirectives)
|
||||
&& !empty($cspValue->value)
|
||||
) {
|
||||
if (\in_array($cspValue->directive, $this->nonceDirectives) && $nonceEnabled) {
|
||||
/**
|
||||
* That line is for B/C we do no longer require to add the nonce tag
|
||||
* but add it once the setting is enabled so this line here is needed
|
||||
* to remove the outdated tag that was required until 4.2.0
|
||||
*/
|
||||
$cspValue->value = str_replace('{nonce}', '', $cspValue->value);
|
||||
|
||||
// Append the nonce when the nonce setting is enabled
|
||||
$cspValue->value = "'nonce-" . $this->cspNonce . "' " . $cspValue->value;
|
||||
}
|
||||
|
||||
// Append the script hashes placeholder
|
||||
if ($scriptHashesEnabled && strpos($cspValue->directive, 'script-src') === 0) {
|
||||
$cspValue->value = '{script-hashes} ' . $cspValue->value;
|
||||
}
|
||||
|
||||
// Append the style hashes placeholder
|
||||
if ($styleHashesEnabled && strpos($cspValue->directive, 'style-src') === 0) {
|
||||
$cspValue->value = '{style-hashes} ' . $cspValue->value;
|
||||
}
|
||||
|
||||
if ($cspValue->directive === 'frame-ancestors') {
|
||||
$frameAncestorsSet = true;
|
||||
}
|
||||
|
||||
// Add strict-dynamic to the script-src directive when enabled
|
||||
if (
|
||||
$strictDynamicEnabled
|
||||
&& $cspValue->directive === 'script-src'
|
||||
&& strpos($cspValue->value, 'strict-dynamic') === false
|
||||
) {
|
||||
$cspValue->value = "'strict-dynamic' " . $cspValue->value;
|
||||
}
|
||||
|
||||
$newCspValues[] = trim($cspValue->directive) . ' ' . trim($cspValue->value);
|
||||
}
|
||||
}
|
||||
|
||||
if ($frameAncestorsSelfEnabled && !$frameAncestorsSet) {
|
||||
$newCspValues[] = "frame-ancestors 'self'";
|
||||
}
|
||||
|
||||
if (empty($newCspValues)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getApplication()->setHeader($cspHeader, trim(implode('; ', $newCspValues)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured static headers.
|
||||
*
|
||||
* @return array We return the array of static headers with its values.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private function getStaticHeaderConfiguration(): array
|
||||
{
|
||||
$staticHeaderConfiguration = [];
|
||||
|
||||
// X-frame-options
|
||||
if ($this->params->get('xframeoptions', 1) === 1) {
|
||||
$staticHeaderConfiguration['x-frame-options#both'] = 'SAMEORIGIN';
|
||||
}
|
||||
|
||||
// Referrer-policy
|
||||
$referrerPolicy = (string) $this->params->get('referrerpolicy', 'strict-origin-when-cross-origin');
|
||||
|
||||
if ($referrerPolicy !== 'disabled') {
|
||||
$staticHeaderConfiguration['referrer-policy#both'] = $referrerPolicy;
|
||||
}
|
||||
|
||||
// Cross-Origin-Opener-Policy
|
||||
$coop = (string) $this->params->get('coop', 'same-origin');
|
||||
|
||||
if ($coop !== 'disabled') {
|
||||
$staticHeaderConfiguration['cross-origin-opener-policy#both'] = $coop;
|
||||
}
|
||||
|
||||
// Generate the strict-transport-security header and make sure the site is SSL
|
||||
if ($this->params->get('hsts', 0) === 1 && Uri::getInstance()->isSsl() === true) {
|
||||
$hstsOptions = [];
|
||||
$hstsOptions[] = 'max-age=' . (int) $this->params->get('hsts_maxage', 31536000);
|
||||
|
||||
if ($this->params->get('hsts_subdomains', 0) === 1) {
|
||||
$hstsOptions[] = 'includeSubDomains';
|
||||
}
|
||||
|
||||
if ($this->params->get('hsts_preload', 0) === 1) {
|
||||
$hstsOptions[] = 'preload';
|
||||
}
|
||||
|
||||
$staticHeaderConfiguration['strict-transport-security#both'] = implode('; ', $hstsOptions);
|
||||
}
|
||||
|
||||
// Generate the additional headers
|
||||
$additionalHttpHeaders = $this->params->get('additional_httpheader', []);
|
||||
|
||||
foreach ($additionalHttpHeaders as $additionalHttpHeader) {
|
||||
// Make sure we have a key and a value
|
||||
if (empty($additionalHttpHeader->key) || empty($additionalHttpHeader->value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make sure the header is a valid and supported header
|
||||
if (!\in_array(strtolower($additionalHttpHeader->key), $this->supportedHttpHeaders)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make sure we do not add one header twice but we support to set a different header per client.
|
||||
if (
|
||||
isset($staticHeaderConfiguration[$additionalHttpHeader->key . '#' . $additionalHttpHeader->client])
|
||||
|| isset($staticHeaderConfiguration[$additionalHttpHeader->key . '#both'])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow the custom csp headers to use the random $cspNonce in the rules
|
||||
if (\in_array(strtolower($additionalHttpHeader->key), ['content-security-policy', 'content-security-policy-report-only'])) {
|
||||
$additionalHttpHeader->value = str_replace('{nonce}', "'nonce-" . $this->cspNonce . "'", $additionalHttpHeader->value);
|
||||
}
|
||||
|
||||
$staticHeaderConfiguration[$additionalHttpHeader->key . '#' . $additionalHttpHeader->client] = $additionalHttpHeader->value;
|
||||
}
|
||||
|
||||
return $staticHeaderConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the static headers when enabled
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
private function setStaticHeaders(): void
|
||||
{
|
||||
$staticHeaderConfiguration = $this->getStaticHeaderConfiguration();
|
||||
|
||||
if (empty($staticHeaderConfiguration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($staticHeaderConfiguration as $headerAndClient => $value) {
|
||||
$headerAndClient = explode('#', $headerAndClient);
|
||||
$header = $headerAndClient[0];
|
||||
$client = $headerAndClient[1] ?? 'both';
|
||||
|
||||
if (!$this->getApplication()->isClient($client) && $client != 'both') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->getApplication()->setHeader($header, $value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user