Current File : /home/pacjaorg/public_html/nsa/plugins/system/httpheaders/httpheaders.php |
<?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
*/
defined('_JEXEC') or die;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
use Joomla\Database\DatabaseDriver;
use Joomla\Event\SubscriberInterface;
/**
* Plugin class for HTTP Headers
*
* @since 4.0.0
*/
class PlgSystemHttpHeaders extends CMSPlugin implements SubscriberInterface
{
/**
* Application object.
*
* @var CMSApplication
* @since 4.0.0
*/
protected $app;
/**
* Database object.
*
* @var DatabaseDriver
* @since 4.0.0
*/
protected $db;
/**
* 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',
];
/**
* 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',
];
/**
* Constructor.
*
* @param object &$subject The object to observe.
* @param array $config An optional associative array of configuration settings.
*
* @since 4.0.0
*/
public function __construct(&$subject, $config)
{
parent::__construct($subject, $config);
// Nonce generation
$this->cspNonce = base64_encode(bin2hex(random_bytes(64)));
$this->app->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
*
* @return void
*
* @since 4.0.0
*/
public function applyHashesToCspRule(): void
{
// CSP is only relevant on html pages. Let's early exit here.
if ($this->app->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->app->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->app->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->app->setHeader($headerConfiguration['name'], $newHeaderValue, true);
}
}
}
/**
* The `setHttpHeaders` method handle the setting of the configured HTTP Headers
*
* @return void
*
* @since 4.0.0
*/
public function setHttpHeaders(): 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->app->isClient($cspClient) || $cspClient === 'both'))
{
$this->setCspHeader();
}
if ($this->app->get('block_floc', 1))
{
$headers = $this->app->getHeaders();
$notPresent = true;
foreach ($headers as $header)
{
if (strtolower($header['name']) === 'permissions-policy')
{
// Append interest-cohort if the Permissions-Policy is not set
if (strpos($header['value'], 'interest-cohort') === false)
{
$this->app->setHeader('Permissions-Policy', $header['value'] . ', interest-cohort=()', true);
}
$notPresent = false;
break;
}
}
if ($notPresent)
{
$this->app->setHeader('Permissions-Policy', 'interest-cohort=()');
}
}
}
/**
* 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->app->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)
{
// Append the nonce
$cspValue->value = str_replace('{nonce}', "'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' ";
}
$newCspValues[] = trim($cspValue->directive) . ' ' . trim($cspValue->value);
}
}
if ($frameAncestorsSelfEnabled && !$frameAncestorsSet)
{
$newCspValues[] = "frame-ancestors 'self'";
}
if (empty($newCspValues))
{
return;
}
$this->app->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 = isset($headerAndClient[1]) ? $headerAndClient[1] : 'both';
if (!$this->app->isClient($client) && $client != 'both')
{
continue;
}
$this->app->setHeader($header, $value, true);
}
}
}