View source
<?php
namespace Drupal\seckit\EventSubscriber;
use Drupal\Component\Utility\Xss;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\seckit\SeckitInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
class SecKitEventSubscriber implements EventSubscriberInterface {
use StringTranslationTrait;
protected $config;
protected $request;
protected $response;
protected $logger;
public function __construct(LoggerInterface $logger, ConfigFactoryInterface $config_factory) {
$this->logger = $logger;
$this->config = $config_factory
->get('seckit.settings');
}
public function onKernelRequest(GetResponseEvent $event) {
$this->request = $event
->getRequest();
if ($this->config
->get('seckit_csrf.origin')) {
$this
->seckitOrigin($event);
}
}
public function onKernelResponse(FilterResponseEvent $event) {
$this->response = $event
->getResponse();
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();
}
$this
->seckitXframe($this->config
->get('seckit_clickjacking.x_frame'), $event);
}
public function seckitOrigin($event) {
$origin = $this->request->headers
->get('Origin');
if (!$origin || $origin === 'null') {
return;
}
if (PHP_SAPI === 'cli') {
return;
}
$method = $this->request
->getMethod();
if (in_array($method, [
'GET',
'HEAD',
], TRUE)) {
return;
}
global $base_root;
$whitelist = explode(',', $this->config
->get('seckit_csrf.origin_whitelist'));
$whitelist[] = $base_root;
$whitelist = array_values(array_filter(array_map('trim', $whitelist)));
if (in_array($origin, $whitelist, TRUE)) {
return;
}
$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));
}
public function seckitCsp() {
$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');
$directives = [];
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)) {
$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';
}
$directives = implode('; ', $directives);
if ($directives) {
if ($csp_report_only) {
$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);
}
}
}
}
public function seckitXxss($setting) {
switch ($setting) {
case SeckitInterface::X_XSS_0:
$this->response->headers
->set('X-XSS-Protection', '0');
break;
case SeckitInterface::X_XSS_1:
$this->response->headers
->set('X-XSS-Protection', '1');
break;
case SeckitInterface::X_XSS_1_BLOCK:
$this->response->headers
->set('X-XSS-Protection', '1; mode=block');
break;
case SeckitInterface::X_XSS_DISABLE:
default:
break;
}
}
public function seckitXframe($setting, $event) {
switch ($setting) {
case SeckitInterface::X_FRAME_SAMEORIGIN:
$this->response->headers
->set('X-Frame-Options', 'SAMEORIGIN');
break;
case SeckitInterface::X_FRAME_DENY:
$this->response->headers
->set('X-Frame-Options', 'DENY');
break;
case SeckitInterface::X_FRAME_ALLOW_FROM:
$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:
$this->response->headers
->remove('X-Frame-Options');
break;
}
}
public static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = [
'onKernelRequest',
100,
];
$events[KernelEvents::RESPONSE][] = [
'onKernelResponse',
];
return $events;
}
public function seckitJsCssNoscript() {
$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);
}
}
public function seckitGetJsCssNoscriptCode($noscript_message = NULL) {
$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;
}
public function seckitHsts() {
$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);
}
public function seckitFromOrigin() {
$value = $this->config
->get('seckit_various.from_origin_destination');
$this->response->headers
->set('From-Origin', $value);
}
public function seckitReferrerPolicy() {
$value = $this->config
->get('seckit_various.referrer_policy_policy');
$this->response->headers
->set('Referrer-Policy', $value);
}
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);
}
public function seckitFeaturePolicy() {
$header[] = $this->config
->get('seckit_fp.feature_policy_policy');
$this->response->headers
->set('Feature-Policy', $header);
}
}