Commit ec733099 authored by Geoff Appleby's avatar Geoff Appleby
Browse files

Issue #3097993: Implement Content Security Policy alter event subscriber

parent e1c4a85c
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -6,3 +6,8 @@ services:
    arguments: ['@config.factory', '@ga.command_registry', '@current_user', '@entity_type.manager']
    tags:
      - { name: event_subscriber }

  ga.csp_subscriber:
    class: Drupal\ga\EventSubscriber\CspSubscriber
    tags:
      - { name: event_subscriber }
+50 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\ga\EventSubscriber;

use Drupal\csp\CspEvents;
use Drupal\csp\Event\PolicyAlterEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Alter CSP policy for Google Analytics.
 */
class CspSubscriber implements EventSubscriberInterface {

  const TRACKING_DOMAIN = 'https://www.google-analytics.com';

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events[CspEvents::POLICY_ALTER] = ['onCspPolicyAlter'];
    return $events;
  }

  /**
   * Alter CSP policy for tracking requests.
   *
   * @param \Drupal\csp\Event\PolicyAlterEvent $alterEvent
   *   The Policy Alter event.
   */
  public function onCspPolicyAlter(PolicyAlterEvent $alterEvent) {
    $policy = $alterEvent->getPolicy();

    if ($policy->hasDirective('img-src')) {
      $policy->appendDirective('img-src', [self::TRACKING_DOMAIN]);
    }
    elseif ($policy->hasDirective('default-src')) {
      $imgDirective = array_merge($policy->getDirective('default-src'), [self::TRACKING_DOMAIN]);
      $policy->setDirective('img-src', $imgDirective);
    }

    if ($policy->hasDirective('connect-src')) {
      $policy->appendDirective('connect-src', [self::TRACKING_DOMAIN]);
    }
    elseif ($policy->hasDirective('default-src')) {
      $connectDirective = array_merge($policy->getDirective('default-src'), [self::TRACKING_DOMAIN]);
      $policy->setDirective('connect-src', $connectDirective);
    }
  }

}
+142 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\ga\Unit\EventSubscriber;

use Drupal\Core\Render\HtmlResponse;
use Drupal\csp\Csp;
use Drupal\csp\CspEvents;
use Drupal\csp\Event\PolicyAlterEvent;
use Drupal\csp\EventSubscriber\CoreCspSubscriber;
use Drupal\ga\EventSubscriber\CspSubscriber;
use Drupal\Tests\UnitTestCase;

/**
 * @coversDefaultClass \Drupal\ga\EventSubscriber\CspSubscriber
 */
class CspSubscriberTest extends UnitTestCase {

  /**
   * The response object.
   *
   * @var \Drupal\Core\Render\HtmlResponse|\PHPUnit\Framework\MockObject\MockObject
   */
  private $response;

  /**
   * {@inheritdoc}
   */
  public function setUp() {
    parent::setUp();

    $this->response = $this->getMockBuilder(HtmlResponse::class)
      ->disableOriginalConstructor()
      ->getMock();
  }

  /**
   * Check that the subscriber listens to the Policy Alter event.
   *
   * @covers ::getSubscribedEvents
   */
  public function testSubscribedEvents() {
    $this->assertArrayHasKey(CspEvents::POLICY_ALTER, CoreCspSubscriber::getSubscribedEvents());
  }

  /**
   * Shouldn't alter the policy if no directives are enabled.
   *
   * @covers ::onCspPolicyAlter
   */
  public function testNoDirectives() {
    $policy = new Csp();
    $alterEvent = new PolicyAlterEvent($policy, $this->response);

    $subscriber = new CspSubscriber();
    $subscriber->onCspPolicyAlter($alterEvent);

    $this->assertFalse($alterEvent->getPolicy()->hasDirective('default-src'));
    $this->assertFalse($alterEvent->getPolicy()->hasDirective('img-src'));
    $this->assertFalse($alterEvent->getPolicy()->hasDirective('connect-src'));
  }

  /**
   * Test that enabled required directives are modified.
   *
   * @covers ::onCspPolicyAlter
   */
  public function testDirectives() {
    $policy = new Csp();
    $policy->setDirective('default-src', [Csp::POLICY_ANY]);
    $policy->setDirective('img-src', [Csp::POLICY_SELF]);
    $policy->setDirective('connect-src', [Csp::POLICY_SELF]);

    $alterEvent = new PolicyAlterEvent($policy, $this->response);

    $subscriber = new CspSubscriber();
    $subscriber->onCspPolicyAlter($alterEvent);

    $this->assertArrayEquals(
      [Csp::POLICY_ANY],
      $alterEvent->getPolicy()->getDirective('default-src')
    );
    $this->assertArrayEquals(
      [Csp::POLICY_SELF, CspSubscriber::TRACKING_DOMAIN],
      $alterEvent->getPolicy()->getDirective('img-src')
    );
    $this->assertArrayEquals(
      [Csp::POLICY_SELF, CspSubscriber::TRACKING_DOMAIN],
      $alterEvent->getPolicy()->getDirective('connect-src')
    );
  }

  /**
   * Test img-src fallback if default-src enabled.
   *
   * @covers ::onCspPolicyAlter
   */
  public function testImgFallback() {
    $policy = new Csp();
    $policy->setDirective('default-src', [Csp::POLICY_SELF]);


    $alterEvent = new PolicyAlterEvent($policy, $this->response);

    $subscriber = new CspSubscriber();
    $subscriber->onCspPolicyAlter($alterEvent);

    $this->assertArrayEquals(
      [Csp::POLICY_SELF],
      $alterEvent->getPolicy()->getDirective('default-src')
    );
    $this->assertArrayEquals(
      [Csp::POLICY_SELF, CspSubscriber::TRACKING_DOMAIN],
      $alterEvent->getPolicy()->getDirective('img-src')
    );
  }

  /**
   * Test connect-src fallback if default-src enabled.
   *
   * @covers ::onCspPolicyAlter
   */
  public function testConnectFallback() {
    $policy = new Csp();
    $policy->setDirective('default-src', [Csp::POLICY_SELF]);


    $alterEvent = new PolicyAlterEvent($policy, $this->response);

    $subscriber = new CspSubscriber();
    $subscriber->onCspPolicyAlter($alterEvent);

    $this->assertArrayEquals(
      [Csp::POLICY_SELF],
      $alterEvent->getPolicy()->getDirective('default-src')
    );
    $this->assertArrayEquals(
      [Csp::POLICY_SELF, CspSubscriber::TRACKING_DOMAIN],
      $alterEvent->getPolicy()->getDirective('connect-src')
    );
  }

}