[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |