You are here

class SecKitEventSubscriber in Security Kit 8

Same name and namespace in other branches
  1. 2.x src/EventSubscriber/SecKitEventSubscriber.php \Drupal\seckit\EventSubscriber\SecKitEventSubscriber

Subscribing an event.

Hierarchy

Expanded class hierarchy of SecKitEventSubscriber

1 string reference to 'SecKitEventSubscriber'
seckit.services.yml in ./seckit.services.yml
seckit.services.yml
1 service uses SecKitEventSubscriber
seckit.subscriber in ./seckit.services.yml
Drupal\seckit\EventSubscriber\SecKitEventSubscriber

File

src/EventSubscriber/SecKitEventSubscriber.php, line 20

Namespace

Drupal\seckit\EventSubscriber
View source
class SecKitEventSubscriber implements EventSubscriberInterface {
  use StringTranslationTrait;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $config;

  /**
   * Request object.
   *
   * @var Request
   */
  protected $request;

  /**
   * Response object.
   *
   * @var \Symfony\Component\HttpFoundation\Response
   */
  protected $response;

  /**
   * Logger instance.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $logger;

  /**
   * Constructs an SecKitEventSubscriber object.
   *
   * @param \Psr\Log\LoggerInterface $logger
   *   The Seckit logger channel.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   */
  public function __construct(LoggerInterface $logger, ConfigFactoryInterface $config_factory) {
    $this->logger = $logger;
    $this->config = $config_factory
      ->get('seckit.settings');
  }

  /**
   * Executes actions on the request event.
   *
   * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
   *   Event Response Object.
   */
  public function onKernelRequest(GetResponseEvent $event) {
    $this->request = $event
      ->getRequest();

    // Execute necessary functions.
    if ($this->config
      ->get('seckit_csrf.origin')) {
      $this
        ->seckitOrigin($event);
    }
  }

  /**
   * Executes actions on the response event.
   *
   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
   *   Filter Response Event object.
   */
  public function onKernelResponse(FilterResponseEvent $event) {
    $this->response = $event
      ->getResponse();

    // Execute necessary functions.
    if ($this->config
      ->get('seckit_xss.csp.checkbox')) {
      $this
        ->seckitCsp();
    }
    if ($this->config
      ->get('seckit_xss.x_xss.select')) {
      $this
        ->seckitXxss($this->config
        ->get('seckit_xss.x_xss.select'));
    }
    if ($this->config
      ->get('seckit_clickjacking.js_css_noscript')) {
      $this
        ->seckitJsCssNoscript();
    }
    if ($this->config
      ->get('seckit_ssl.hsts')) {
      $this
        ->seckitHsts();
    }
    if ($this->config
      ->get('seckit_various.from_origin')) {
      $this
        ->seckitFromOrigin();
    }
    if ($this->config
      ->get('seckit_various.referrer_policy')) {
      $this
        ->seckitReferrerPolicy();
    }
    if ($this->config
      ->get('seckit_ct.expect_ct')) {
      $this
        ->seckitExpectCt();
    }
    if ($this->config
      ->get('seckit_fp.feature_policy')) {
      $this
        ->seckitFeaturePolicy();
    }

    // Always call this (regardless of the setting) since if it's disabled it
    // may be necessary to actively disable the core's clickjacking defense.
    $this
      ->seckitXframe($this->config
      ->get('seckit_clickjacking.x_frame'), $event);
  }

  /**
   * Aborts HTTP request upon invalid 'Origin' HTTP request header.
   *
   * When included in an HTTP request, the Origin header indicates the origin(s)
   * that caused the user agent to issue the request. This helps to protect
   * against CSRF attacks, as we can abort requests with an unapproved origin.
   *
   * Applies to all HTTP request methods except GET and HEAD.
   *
   * Requests which do not include an 'Origin' header must always be allowed,
   * as (a) not all user-agents support the header, and (b) those that do may
   * include it or omit it at their discretion.
   *
   * Note that (a) will become progressively less of a factor over time --
   * CSRF attacks depend upon convincing a user agent to send a request, and
   * there is no particular motivation for users to prevent their web browsers
   * from sending this header; so as people upgrade to browsers which support
   * 'Origin', its effectiveness increases.
   *
   * Implementation of Origin is based on specification draft available at
   * http://tools.ietf.org/html/draft-abarth-origin-09
   */
  public function seckitOrigin($event) {

    // Allow requests without an 'Origin' header, or with a 'null' origin.
    $origin = $this->request->headers
      ->get('Origin');
    if (!$origin || $origin === 'null') {
      return;
    }

    // Allow command-line requests.
    if (PHP_SAPI === 'cli') {
      return;
    }

    // Allow GET and HEAD requests.
    $method = $this->request
      ->getMethod();
    if (in_array($method, [
      'GET',
      'HEAD',
    ], TRUE)) {
      return;
    }

    // Allow requests from whitelisted Origins.
    global $base_root;
    $whitelist = explode(',', $this->config
      ->get('seckit_csrf.origin_whitelist'));

    // Default origin is always allowed.
    $whitelist[] = $base_root;
    $whitelist = array_values(array_filter(array_map('trim', $whitelist)));
    if (in_array($origin, $whitelist, TRUE)) {
      return;

      // n.b. RFC 6454 allows Origins to have more than one value (each
      // separated by a single space).  All values must be on the whitelist
      // (order is not important).  We intentionally do not handle this
      // because the feature has been confirmed as a design mistake which
      // user agents do not utilise in practice.  For details, see
      // http://lists.w3.org/Archives/Public/www-archive/2012Jun/0001.html
      // and https://www.drupal.org/node/2406075
    }

    // The Origin is invalid, so we deny the request.
    // Clean the POST data first, as drupal_access_denied() may render a page
    // with forms which check for their submissions.
    $args = [
      '@ip' => $this->request
        ->getClientIp(),
      '@origin' => $origin,
    ];
    $message = 'Possible CSRF attack was blocked. IP address: @ip, Origin: @origin.';
    $this->logger
      ->warning($message, $args);
    $event
      ->setResponse(new Response($this
      ->t('Access denied'), Response::HTTP_FORBIDDEN));
  }

  /**
   * Sends Content Security Policy HTTP headers.
   *
   * Header specifies Content Security Policy (CSP) for a website,
   * which is used to allow/block content from selected sources.
   *
   * Based on specification available at http://www.w3.org/TR/CSP/
   */
  public function seckitCsp() {

    // Get default/set options.
    $csp_vendor_prefix_x = $this->config
      ->get('seckit_xss.csp.vendor-prefix.x');
    $csp_vendor_prefix_webkit = $this->config
      ->get('seckit_xss.csp.vendor-prefix.webkit');
    $csp_report_only = $this->config
      ->get('seckit_xss.csp.report-only');
    $csp_default_src = $this->config
      ->get('seckit_xss.csp.default-src');
    $csp_script_src = $this->config
      ->get('seckit_xss.csp.script-src');
    $csp_object_src = $this->config
      ->get('seckit_xss.csp.object-src');
    $csp_img_src = $this->config
      ->get('seckit_xss.csp.img-src');
    $csp_media_src = $this->config
      ->get('seckit_xss.csp.media-src');
    $csp_style_src = $this->config
      ->get('seckit_xss.csp.style-src');
    $csp_frame_src = $this->config
      ->get('seckit_xss.csp.frame-src');
    $csp_frame_ancestors = $this->config
      ->get('seckit_xss.csp.frame-ancestors');
    $csp_child_src = $this->config
      ->get('seckit_xss.csp.child-src');
    $csp_font_src = $this->config
      ->get('seckit_xss.csp.font-src');
    $csp_connect_src = $this->config
      ->get('seckit_xss.csp.connect-src');
    $csp_report_uri = $this->config
      ->get('seckit_xss.csp.report-uri');
    $csp_upgrade_req = $this->config
      ->get('seckit_xss.csp.upgrade-req');

    // $csp_policy_uri = $this->config->get('seckit_xss.csp.policy-uri');
    // Prepare directives.
    $directives = [];

    // If policy-uri is declared, no other directives are permitted.

    /* if ($csp_report_only) {
       $directives = "policy-uri " . base_path() . $csp_report_only;
       } */

    // Otherwise prepare directives.
    // else {.
    if ($csp_default_src) {
      $directives[] = "default-src {$csp_default_src}";
    }
    if ($csp_script_src) {
      $directives[] = "script-src {$csp_script_src}";
    }
    if ($csp_object_src) {
      $directives[] = "object-src {$csp_object_src}";
    }
    if ($csp_style_src) {
      $directives[] = "style-src {$csp_style_src}";
    }
    if ($csp_img_src) {
      $directives[] = "img-src {$csp_img_src}";
    }
    if ($csp_media_src) {
      $directives[] = "media-src {$csp_media_src}";
    }
    if ($csp_frame_src) {
      $directives[] = "frame-src {$csp_frame_src}";
    }
    if ($csp_frame_ancestors) {
      $directives[] = "frame-ancestors {$csp_frame_ancestors}";
    }
    if ($csp_child_src) {
      $directives[] = "child-src {$csp_child_src}";
    }
    if ($csp_font_src) {
      $directives[] = "font-src {$csp_font_src}";
    }
    if ($csp_connect_src) {
      $directives[] = "connect-src {$csp_connect_src}";
    }
    if ($csp_report_uri) {
      $base_path = '';
      if (!UrlHelper::isExternal($csp_report_uri)) {

        // Strip leading slashes from internal paths to prevent them becoming
        // external URLs without protocol. /report-csp-violation should not be
        // turned into //report-csp-violation
        $csp_report_uri = ltrim($csp_report_uri, '/');
        $base_path = base_path();
      }
      $directives[] = "report-uri " . $base_path . $csp_report_uri;
    }
    if ($csp_upgrade_req) {
      $directives[] = 'upgrade-insecure-requests';
    }

    // Merge directives.
    $directives = implode('; ', $directives);

    // }
    // send HTTP response header if directives were prepared.
    if ($directives) {
      if ($csp_report_only) {

        // Use report-only mode.
        $this->response->headers
          ->set('Content-Security-Policy-Report-Only', $directives);
        if ($csp_vendor_prefix_x) {
          $this->response->headers
            ->set('X-Content-Security-Policy-Report-Only', $directives);
        }
        if ($csp_vendor_prefix_webkit) {
          $this->response->headers
            ->set('X-WebKit-CSP-Report-Only', $directives);
        }
      }
      else {
        $this->response->headers
          ->set('Content-Security-Policy', $directives);
        if ($csp_vendor_prefix_x) {
          $this->response->headers
            ->set('X-Content-Security-Policy', $directives);
        }
        if ($csp_vendor_prefix_webkit) {
          $this->response->headers
            ->set('X-WebKit-CSP', $directives);
        }
      }
    }
  }

  /**
   * Sends X-XSS-Protection HTTP header.
   *
   * X-XSS-Protection controls IE8/Safari/Chrome internal XSS filter.
   */
  public function seckitXxss($setting) {
    switch ($setting) {
      case SeckitInterface::X_XSS_0:

        // Set X-XSS-Protection header to 0.
        $this->response->headers
          ->set('X-XSS-Protection', '0');
        break;
      case SeckitInterface::X_XSS_1:

        // Set X-XSS-Protection header to 1.
        $this->response->headers
          ->set('X-XSS-Protection', '1');
        break;
      case SeckitInterface::X_XSS_1_BLOCK:

        // Set X-XSS-Protection header to 1; mode=block.
        $this->response->headers
          ->set('X-XSS-Protection', '1; mode=block');
        break;
      case SeckitInterface::X_XSS_DISABLE:

      // Do nothing.
      default:
        break;
    }
  }

  /**
   * Sends X-Frame-Options HTTP header.
   *
   * X-Frame-Options controls should browser show frames or not.
   * More information can be found at initial article about it at
   * http://blogs.msdn.com/ie/archive/2009/01/27/ie8-security-part-vii-clickjacking-defenses.aspx.
   *
   * Implementation of X-Frame-Options is based on specification draft availabe
   * at http://tools.ietf.org/html/draft-ietf-websec-x-frame-options-01.
   */
  public function seckitXframe($setting, $event) {
    switch ($setting) {
      case SeckitInterface::X_FRAME_SAMEORIGIN:

        // Set X-Frame-Options to SAMEORIGIN.
        $this->response->headers
          ->set('X-Frame-Options', 'SAMEORIGIN');
        break;
      case SeckitInterface::X_FRAME_DENY:

        // Set X-Frame-Options to DENY.
        $this->response->headers
          ->set('X-Frame-Options', 'DENY');
        break;
      case SeckitInterface::X_FRAME_ALLOW_FROM:

        // If this request's Origin is allowed, we specify that value.
        // If the origin is not allowed, we can use any other value to prevent
        // the client from framing the page.
        $allowed_from = $this->config
          ->get('seckit_clickjacking.x_frame_allow_from');
        $values = explode("\n", $allowed_from);
        $allowed = array_values(array_filter(array_map('trim', $values)));
        $origin = $event
          ->getRequest()->headers
          ->get('Origin');
        if (!in_array($origin, $allowed, TRUE)) {
          $origin = array_pop($allowed);
        }
        $this->response->headers
          ->set('X-Frame-Options', "ALLOW-FROM {$origin}");
        break;
      case SeckitInterface::X_FRAME_DISABLE:

        // Make sure Drupal core does not set the header either.
        // See Drupal\Core\EventSubscriber\FinishResponseSubscriber.
        $this->response->headers
          ->remove('X-Frame-Options');
        break;
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = [
      'onKernelRequest',
      100,
    ];
    $events[KernelEvents::RESPONSE][] = [
      'onKernelResponse',
    ];
    return $events;
  }

  /**
   * Enables JavaScript + CSS + Noscript Clickjacking defense.
   *
   * Closes inline JavaScript and allows loading of any inline HTML elements.
   * After, it starts new inline JavaScript to avoid breaking syntax.
   * We need it, because Drupal API doesn't allow to init HTML elements in
   * desired sequence.
   */
  public function seckitJsCssNoscript() {

    // @todo Consider batter solution?
    $content = $this->response
      ->getContent();
    $head_close_position = strpos($content, '</head>');
    if ($head_close_position) {
      $content = substr_replace($content, $this
        ->seckitGetJsCssNoscriptCode(), $head_close_position, 0);
      $this->response
        ->setContent($content);
    }
  }

  /**
   * Gets JavaScript and CSS code.
   *
   * @return string
   *   Return the js and css code.
   */
  public function seckitGetJsCssNoscriptCode($noscript_message = NULL) {

    // Allows noscript automated testing.
    $noscript_message = $noscript_message ? $noscript_message : $this->config
      ->get('seckit_clickjacking.noscript_message');
    $message = Xss::filter($noscript_message);
    $path = base_path() . drupal_get_path('module', 'seckit');
    return <<<EOT
        <script type="text/javascript" src="{<span class="php-variable">$path</span>}/js/seckit.document_write.js"></script>
        <link type="text/css" rel="stylesheet" id="seckit-clickjacking-no-body" media="all" href="{<span class="php-variable">$path</span>}/css/seckit.no_body.css" />
        <!-- stop SecKit protection -->
        <noscript>
        <link type="text/css" rel="stylesheet" id="seckit-clickjacking-noscript-tag" media="all" href="{<span class="php-variable">$path</span>}/css/seckit.noscript_tag.css" />
        <div id="seckit-noscript-tag">
          {<span class="php-variable">$message</span>}
        </div>
        </noscript>
EOT;
  }

  /**
   * Sends HTTP Strict-Transport-Security header (HSTS).
   *
   * The HSTS header prevents certain eavesdropping and MITM attacks like
   * SSLStrip. It forces the user-agent to send requests in HTTPS-only mode.
   * e.g.: http:// links are treated as https://
   *
   * Implementation of HSTS is based on the specification draft available at
   * http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
   */
  public function seckitHsts() {

    // Prepare HSTS header value.
    $header[] = sprintf("max-age=%d", $this->config
      ->get('seckit_ssl.hsts_max_age'));
    if ($this->config
      ->get('seckit_ssl.hsts_subdomains')) {
      $header[] = 'includeSubDomains';
    }
    if ($this->config
      ->get('seckit_ssl.hsts_preload')) {
      $header[] = 'preload';
    }
    $header = implode('; ', $header);
    $this->response->headers
      ->set('Strict-Transport-Security', $header);
  }

  /**
   * Sends From-Origin HTTP response header.
   *
   * Implementation is based on specification draft
   * available at http://www.w3.org/TR/from-origin.
   */
  public function seckitFromOrigin() {
    $value = $this->config
      ->get('seckit_various.from_origin_destination');
    $this->response->headers
      ->set('From-Origin', $value);
  }

  /**
   * Sends Referrer-Policy HTTP response header.
   *
   * Implementation is based on specification draft
   * available at https://www.w3.org/TR/referrer-policy.
   */
  public function seckitReferrerPolicy() {
    $value = $this->config
      ->get('seckit_various.referrer_policy_policy');
    $this->response->headers
      ->set('Referrer-Policy', $value);
  }

  /**
   * Sends Expect-CT HTTP response header.
   *
   * Implementation is based on specification draft available at
   * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT.
   */
  public function seckitExpectCt() {
    $header[] = sprintf("max-age=%d", $this->config
      ->get('seckit_ct.max_age'));
    if ($this->config
      ->get('seckit_ct.enforce')) {
      $header[] = 'enforce';
    }
    if ($this->config
      ->get('seckit_ct.report_uri')) {
      $header[] = 'report-uri="' . $this->config
        ->get('seckit_ct.report_uri') . '"';
    }
    $header = implode(', ', $header);
    $this->response->headers
      ->set('Expect-CT', $header);
  }

  /**
   * Sends Feature-Policy HTTP response header.
   *
   * Implementation is based on specification draft available
   * at https://developers.google.com/web/updates/2018/06/feature-policy.
   */
  public function seckitFeaturePolicy() {
    $header[] = $this->config
      ->get('seckit_fp.feature_policy_policy');
    $this->response->headers
      ->set('Feature-Policy', $header);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
SecKitEventSubscriber::$config protected property The config factory.
SecKitEventSubscriber::$logger protected property Logger instance.
SecKitEventSubscriber::$request protected property Request object.
SecKitEventSubscriber::$response protected property Response object.
SecKitEventSubscriber::getSubscribedEvents public static function Returns an array of event names this subscriber wants to listen to.
SecKitEventSubscriber::onKernelRequest public function Executes actions on the request event.
SecKitEventSubscriber::onKernelResponse public function Executes actions on the response event.
SecKitEventSubscriber::seckitCsp public function Sends Content Security Policy HTTP headers.
SecKitEventSubscriber::seckitExpectCt public function Sends Expect-CT HTTP response header.
SecKitEventSubscriber::seckitFeaturePolicy public function Sends Feature-Policy HTTP response header.
SecKitEventSubscriber::seckitFromOrigin public function Sends From-Origin HTTP response header.
SecKitEventSubscriber::seckitGetJsCssNoscriptCode public function Gets JavaScript and CSS code.
SecKitEventSubscriber::seckitHsts public function Sends HTTP Strict-Transport-Security header (HSTS).
SecKitEventSubscriber::seckitJsCssNoscript public function Enables JavaScript + CSS + Noscript Clickjacking defense.
SecKitEventSubscriber::seckitOrigin public function Aborts HTTP request upon invalid 'Origin' HTTP request header.
SecKitEventSubscriber::seckitReferrerPolicy public function Sends Referrer-Policy HTTP response header.
SecKitEventSubscriber::seckitXframe public function Sends X-Frame-Options HTTP header.
SecKitEventSubscriber::seckitXxss public function Sends X-XSS-Protection HTTP header.
SecKitEventSubscriber::__construct public function Constructs an SecKitEventSubscriber object.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.