[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/system/httpheaders/ -> httpheaders.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  System.HttpHeaders
   6   *
   7   * @copyright   (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9  
  10   * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
  11   */
  12  
  13  use Joomla\CMS\Application\CMSApplication;
  14  use Joomla\CMS\Plugin\CMSPlugin;
  15  use Joomla\CMS\Uri\Uri;
  16  use Joomla\Database\DatabaseDriver;
  17  use Joomla\Event\DispatcherInterface;
  18  use Joomla\Event\Event;
  19  use Joomla\Event\SubscriberInterface;
  20  
  21  // phpcs:disable PSR1.Files.SideEffects
  22  \defined('_JEXEC') or die;
  23  // phpcs:enable PSR1.Files.SideEffects
  24  
  25  /**
  26   * Plugin class for HTTP Headers
  27   *
  28   * @since  4.0.0
  29   */
  30  class PlgSystemHttpHeaders extends CMSPlugin implements SubscriberInterface
  31  {
  32      /**
  33       * Application object.
  34       *
  35       * @var    CMSApplication
  36       * @since  4.0.0
  37       */
  38      protected $app;
  39  
  40      /**
  41       * Database object.
  42       *
  43       * @var    DatabaseDriver
  44       * @since  4.0.0
  45       */
  46      protected $db;
  47  
  48      /**
  49       * The generated csp nonce value
  50       *
  51       * @var    string
  52       * @since  4.0.0
  53       */
  54      private $cspNonce;
  55  
  56      /**
  57       * The list of the supported HTTP headers
  58       *
  59       * @var    array
  60       * @since  4.0.0
  61       */
  62      private $supportedHttpHeaders = [
  63          'strict-transport-security',
  64          'content-security-policy',
  65          'content-security-policy-report-only',
  66          'x-frame-options',
  67          'referrer-policy',
  68          'expect-ct',
  69          'feature-policy',
  70          'cross-origin-opener-policy',
  71          'report-to',
  72          'permissions-policy',
  73      ];
  74  
  75      /**
  76       * The list of valid directives based on: https://www.w3.org/TR/CSP3/#csp-directives
  77       *
  78       * @var    array
  79       * @since  4.0.0
  80       */
  81      private $validDirectives = [
  82          'child-src',
  83          'connect-src',
  84          'default-src',
  85          'font-src',
  86          'frame-src',
  87          'img-src',
  88          'manifest-src',
  89          'media-src',
  90          'prefetch-src',
  91          'object-src',
  92          'script-src',
  93          'script-src-elem',
  94          'script-src-attr',
  95          'style-src',
  96          'style-src-elem',
  97          'style-src-attr',
  98          'worker-src',
  99          'base-uri',
 100          'plugin-types',
 101          'sandbox',
 102          'form-action',
 103          'frame-ancestors',
 104          'navigate-to',
 105          'report-uri',
 106          'report-to',
 107          'block-all-mixed-content',
 108          'upgrade-insecure-requests',
 109          'require-sri-for',
 110      ];
 111  
 112      /**
 113       * The list of directives without a value
 114       *
 115       * @var    array
 116       * @since  4.0.0
 117       */
 118      private $noValueDirectives = [
 119          'block-all-mixed-content',
 120          'upgrade-insecure-requests',
 121      ];
 122  
 123      /**
 124       * The list of directives supporting nonce
 125       *
 126       * @var    array
 127       * @since  4.0.0
 128       */
 129      private $nonceDirectives = [
 130          'script-src',
 131          'style-src',
 132      ];
 133  
 134      /**
 135       * Constructor.
 136       *
 137       * @param   DispatcherInterface  $subject  The object to observe.
 138       * @param   array                $config   An optional associative array of configuration settings.
 139       *
 140       * @since   4.0.0
 141       */
 142      public function __construct(&$subject, $config)
 143      {
 144          parent::__construct($subject, $config);
 145  
 146          $nonceEnabled = (int) $this->params->get('nonce_enabled', 0);
 147  
 148          // Nonce generation when it's enabled
 149          if ($nonceEnabled) {
 150              $this->cspNonce = base64_encode(bin2hex(random_bytes(64)));
 151          }
 152  
 153          // Set the nonce, when not set we set it to NULL which is checked down the line
 154          $this->app->set('csp_nonce', $this->cspNonce);
 155      }
 156  
 157      /**
 158       * Returns an array of events this subscriber will listen to.
 159       *
 160       * @return  array
 161       *
 162       * @since   4.0.0
 163       */
 164      public static function getSubscribedEvents(): array
 165      {
 166          return [
 167              'onAfterInitialise' => 'setHttpHeaders',
 168              'onAfterRender'     => 'applyHashesToCspRule',
 169          ];
 170      }
 171  
 172      /**
 173       * The `applyHashesToCspRule` method makes sure the csp hashes are added to the csp header when enabled
 174       *
 175       * @return  void
 176       *
 177       * @since   4.0.0
 178       */
 179      public function applyHashesToCspRule(Event $event): void
 180      {
 181          // CSP is only relevant on html pages. Let's early exit here.
 182          if ($this->app->getDocument()->getType() !== 'html') {
 183              return;
 184          }
 185  
 186          $scriptHashesEnabled = (int) $this->params->get('script_hashes_enabled', 0);
 187          $styleHashesEnabled  = (int) $this->params->get('style_hashes_enabled', 0);
 188  
 189          // Early exit when both options are disabled
 190          if (!$scriptHashesEnabled && !$styleHashesEnabled) {
 191              return;
 192          }
 193  
 194          $headData     = $this->app->getDocument()->getHeadData();
 195          $scriptHashes = [];
 196          $styleHashes  = [];
 197  
 198          if ($scriptHashesEnabled) {
 199              // Generate the hashes for the script-src
 200              $inlineScripts = is_array($headData['script']) ? $headData['script'] : [];
 201  
 202              foreach ($inlineScripts as $type => $scripts) {
 203                  foreach ($scripts as $hash => $scriptContent) {
 204                      $scriptHashes[] = "'sha256-" . base64_encode(hash('sha256', $scriptContent, true)) . "'";
 205                  }
 206              }
 207          }
 208  
 209          if ($styleHashesEnabled) {
 210              // Generate the hashes for the style-src
 211              $inlineStyles = is_array($headData['style']) ? $headData['style'] : [];
 212  
 213              foreach ($inlineStyles as $type => $styles) {
 214                  foreach ($styles as $hash => $styleContent) {
 215                      $styleHashes[] = "'sha256-" . base64_encode(hash('sha256', $styleContent, true)) . "'";
 216                  }
 217              }
 218          }
 219  
 220          // Replace the hashes in the csp header when set.
 221          $headers = $this->app->getHeaders();
 222  
 223          foreach ($headers as $id => $headerConfiguration) {
 224              if (
 225                  strtolower($headerConfiguration['name']) === 'content-security-policy'
 226                  || strtolower($headerConfiguration['name']) === 'content-security-policy-report-only'
 227              ) {
 228                  $newHeaderValue = $headerConfiguration['value'];
 229  
 230                  if (!empty($scriptHashes)) {
 231                      $newHeaderValue = str_replace('{script-hashes}', implode(' ', $scriptHashes), $newHeaderValue);
 232                  } else {
 233                      $newHeaderValue = str_replace('{script-hashes}', '', $newHeaderValue);
 234                  }
 235  
 236                  if (!empty($styleHashes)) {
 237                      $newHeaderValue = str_replace('{style-hashes}', implode(' ', $styleHashes), $newHeaderValue);
 238                  } else {
 239                      $newHeaderValue = str_replace('{style-hashes}', '', $newHeaderValue);
 240                  }
 241  
 242                  $this->app->setHeader($headerConfiguration['name'], $newHeaderValue, true);
 243              }
 244          }
 245      }
 246  
 247      /**
 248       * The `setHttpHeaders` method handle the setting of the configured HTTP Headers
 249       *
 250       * @return  void
 251       *
 252       * @since   4.0.0
 253       */
 254      public function setHttpHeaders(Event $event): void
 255      {
 256          // Set the default header when they are enabled
 257          $this->setStaticHeaders();
 258  
 259          // Handle CSP Header configuration
 260          $cspEnabled = (int) $this->params->get('contentsecuritypolicy', 0);
 261          $cspClient  = (string) $this->params->get('contentsecuritypolicy_client', 'site');
 262  
 263          // Check whether CSP is enabled and enabled by the current client
 264          if ($cspEnabled && ($this->app->isClient($cspClient) || $cspClient === 'both')) {
 265              $this->setCspHeader();
 266          }
 267      }
 268  
 269      /**
 270       * Set the CSP header when enabled
 271       *
 272       * @return  void
 273       *
 274       * @since   4.0.0
 275       */
 276      private function setCspHeader(): void
 277      {
 278          $cspReadOnly = (int) $this->params->get('contentsecuritypolicy_report_only', 1);
 279          $cspHeader   = $cspReadOnly === 0 ? 'content-security-policy' : 'content-security-policy-report-only';
 280  
 281          // In custom mode we compile the header from the values configured
 282          $cspValues                 = $this->params->get('contentsecuritypolicy_values', []);
 283          $nonceEnabled              = (int) $this->params->get('nonce_enabled', 0);
 284          $scriptHashesEnabled       = (int) $this->params->get('script_hashes_enabled', 0);
 285          $strictDynamicEnabled      = (int) $this->params->get('strict_dynamic_enabled', 0);
 286          $styleHashesEnabled        = (int) $this->params->get('style_hashes_enabled', 0);
 287          $frameAncestorsSelfEnabled = (int) $this->params->get('frame_ancestors_self_enabled', 1);
 288          $frameAncestorsSet         = false;
 289  
 290          foreach ($cspValues as $cspValue) {
 291              // Handle the client settings foreach header
 292              if (!$this->app->isClient($cspValue->client) && $cspValue->client != 'both') {
 293                  continue;
 294              }
 295  
 296              // Handle non value directives
 297              if (in_array($cspValue->directive, $this->noValueDirectives)) {
 298                  $newCspValues[] = trim($cspValue->directive);
 299  
 300                  continue;
 301              }
 302  
 303              // We can only use this if this is a valid entry
 304              if (
 305                  in_array($cspValue->directive, $this->validDirectives)
 306                  && !empty($cspValue->value)
 307              ) {
 308                  if (in_array($cspValue->directive, $this->nonceDirectives) && $nonceEnabled) {
 309                      /**
 310                       * That line is for B/C we do no longer require to add the nonce tag
 311                       * but add it once the setting is enabled so this line here is needed
 312                       * to remove the outdated tag that was required until 4.2.0
 313                       */
 314                      $cspValue->value = str_replace('{nonce}', '', $cspValue->value);
 315  
 316                      // Append the nonce when the nonce setting is enabled
 317                      $cspValue->value = "'nonce-" . $this->cspNonce . "' " . $cspValue->value;
 318                  }
 319  
 320                  // Append the script hashes placeholder
 321                  if ($scriptHashesEnabled && strpos($cspValue->directive, 'script-src') === 0) {
 322                      $cspValue->value = '{script-hashes} ' . $cspValue->value;
 323                  }
 324  
 325                  // Append the style hashes placeholder
 326                  if ($styleHashesEnabled && strpos($cspValue->directive, 'style-src') === 0) {
 327                      $cspValue->value = '{style-hashes} ' . $cspValue->value;
 328                  }
 329  
 330                  if ($cspValue->directive === 'frame-ancestors') {
 331                      $frameAncestorsSet = true;
 332                  }
 333  
 334                  // Add strict-dynamic to the script-src directive when enabled
 335                  if (
 336                      $strictDynamicEnabled
 337                      && $cspValue->directive === 'script-src'
 338                      && strpos($cspValue->value, 'strict-dynamic') === false
 339                  ) {
 340                      $cspValue->value = "'strict-dynamic' " . $cspValue->value;
 341                  }
 342  
 343                  $newCspValues[] = trim($cspValue->directive) . ' ' . trim($cspValue->value);
 344              }
 345          }
 346  
 347          if ($frameAncestorsSelfEnabled && !$frameAncestorsSet) {
 348              $newCspValues[] = "frame-ancestors 'self'";
 349          }
 350  
 351          if (empty($newCspValues)) {
 352              return;
 353          }
 354  
 355          $this->app->setHeader($cspHeader, trim(implode('; ', $newCspValues)));
 356      }
 357  
 358      /**
 359       * Get the configured static headers.
 360       *
 361       * @return  array  We return the array of static headers with its values.
 362       *
 363       * @since   4.0.0
 364       */
 365      private function getStaticHeaderConfiguration(): array
 366      {
 367          $staticHeaderConfiguration = [];
 368  
 369          // X-frame-options
 370          if ($this->params->get('xframeoptions', 1) === 1) {
 371              $staticHeaderConfiguration['x-frame-options#both'] = 'SAMEORIGIN';
 372          }
 373  
 374          // Referrer-policy
 375          $referrerPolicy = (string) $this->params->get('referrerpolicy', 'strict-origin-when-cross-origin');
 376  
 377          if ($referrerPolicy !== 'disabled') {
 378              $staticHeaderConfiguration['referrer-policy#both'] = $referrerPolicy;
 379          }
 380  
 381          // Cross-Origin-Opener-Policy
 382          $coop = (string) $this->params->get('coop', 'same-origin');
 383  
 384          if ($coop !== 'disabled') {
 385              $staticHeaderConfiguration['cross-origin-opener-policy#both'] = $coop;
 386          }
 387  
 388          // Generate the strict-transport-security header and make sure the site is SSL
 389          if ($this->params->get('hsts', 0) === 1 && Uri::getInstance()->isSsl() === true) {
 390              $hstsOptions   = [];
 391              $hstsOptions[] = 'max-age=' . (int) $this->params->get('hsts_maxage', 31536000);
 392  
 393              if ($this->params->get('hsts_subdomains', 0) === 1) {
 394                  $hstsOptions[] = 'includeSubDomains';
 395              }
 396  
 397              if ($this->params->get('hsts_preload', 0) === 1) {
 398                  $hstsOptions[] = 'preload';
 399              }
 400  
 401              $staticHeaderConfiguration['strict-transport-security#both'] = implode('; ', $hstsOptions);
 402          }
 403  
 404          // Generate the additional headers
 405          $additionalHttpHeaders = $this->params->get('additional_httpheader', []);
 406  
 407          foreach ($additionalHttpHeaders as $additionalHttpHeader) {
 408              // Make sure we have a key and a value
 409              if (empty($additionalHttpHeader->key) || empty($additionalHttpHeader->value)) {
 410                  continue;
 411              }
 412  
 413              // Make sure the header is a valid and supported header
 414              if (!in_array(strtolower($additionalHttpHeader->key), $this->supportedHttpHeaders)) {
 415                  continue;
 416              }
 417  
 418              // Make sure we do not add one header twice but we support to set a different header per client.
 419              if (
 420                  isset($staticHeaderConfiguration[$additionalHttpHeader->key . '#' . $additionalHttpHeader->client])
 421                  || isset($staticHeaderConfiguration[$additionalHttpHeader->key . '#both'])
 422              ) {
 423                  continue;
 424              }
 425  
 426              // Allow the custom csp headers to use the random $cspNonce in the rules
 427              if (in_array(strtolower($additionalHttpHeader->key), ['content-security-policy', 'content-security-policy-report-only'])) {
 428                  $additionalHttpHeader->value = str_replace('{nonce}', "'nonce-" . $this->cspNonce . "'", $additionalHttpHeader->value);
 429              }
 430  
 431              $staticHeaderConfiguration[$additionalHttpHeader->key . '#' . $additionalHttpHeader->client] = $additionalHttpHeader->value;
 432          }
 433  
 434          return $staticHeaderConfiguration;
 435      }
 436  
 437      /**
 438       * Set the static headers when enabled
 439       *
 440       * @return  void
 441       *
 442       * @since   4.0.0
 443       */
 444      private function setStaticHeaders(): void
 445      {
 446          $staticHeaderConfiguration = $this->getStaticHeaderConfiguration();
 447  
 448          if (empty($staticHeaderConfiguration)) {
 449              return;
 450          }
 451  
 452          foreach ($staticHeaderConfiguration as $headerAndClient => $value) {
 453              $headerAndClient = explode('#', $headerAndClient);
 454              $header = $headerAndClient[0];
 455              $client = $headerAndClient[1] ?? 'both';
 456  
 457              if (!$this->app->isClient($client) && $client != 'both') {
 458                  continue;
 459              }
 460  
 461              $this->app->setHeader($header, $value, true);
 462          }
 463      }
 464  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer