Passed
Push — master ( a50086...2b3992 )
by Guy
02:25
created

SecurityHeaderMiddleware::updateCspHeader()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 9
c 1
b 0
f 0
nc 7
nop 1
dl 0
loc 20
rs 9.6111
1
<?php
2
3
namespace Signify\Middleware;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\Control\Middleware\HTTPMiddleware;
8
use SilverStripe\Core\Config\Configurable;
9
use SilverStripe\Core\Extensible;
10
use SilverStripe\SiteConfig\SiteConfig;
11
12
class SecurityHeaderMiddleware implements HTTPMiddleware
13
{
14
    use Configurable;
15
    use Extensible;
16
17
    /**
18
     * An array of HTTP headers.
19
     * @config
20
     * @var array
21
     */
22
    private static $headers = [
0 ignored issues
show
introduced by
The private property $headers is not used, and could be removed.
Loading history...
23
        'global' => array(),
24
    ];
25
26
    /**
27
     * Whether to automatically add the CMS report endpoint to the CSP config.
28
     * @config
29
     * @var string
30
     */
31
    private static $enable_reporting = true;
0 ignored issues
show
introduced by
The private property $enable_reporting is not used, and could be removed.
Loading history...
32
33
    /**
34
     * The URI to report CSP violations to.
35
     * See routes.yml
36
     * @config
37
     * @var string
38
     */
39
    private static $report_uri = 'cspviolations/report';
0 ignored issues
show
introduced by
The private property $report_uri is not used, and could be removed.
Loading history...
40
41
    /**
42
     * Whether to use the report-to header and CSP directive.
43
     * @config
44
     * @var string
45
     */
46
    private static $use_report_to = false;
0 ignored issues
show
introduced by
The private property $use_report_to is not used, and could be removed.
Loading history...
47
48
    /**
49
     * Whether subdomains should report to the same endpoint.
50
     * @config
51
     * @var string
52
     */
53
    private static $report_to_subdomains = false;
0 ignored issues
show
introduced by
The private property $report_to_subdomains is not used, and could be removed.
Loading history...
54
55
    /**
56
     * The group name for the report-to CSP directive.
57
     * @config
58
     * @var string
59
     */
60
    private static $report_to_group = 'signify-csp-violation';
0 ignored issues
show
introduced by
The private property $report_to_group is not used, and could be removed.
Loading history...
61
62
    public function process(HTTPRequest $request, callable $delegate)
63
    {
64
        $response = $delegate($request);
65
66
        $headersConfig = (array) $this->config()->get('headers');
67
        if (empty($headersConfig['global'])) {
68
            return $response;
69
        }
70
71
        $headersToSend = $headersConfig['global'];
72
        if ($this->config()->get('enable_reporting') && $this->config()->get('use_report_to')) {
73
            $this->addReportToHeader($headersToSend);
74
        }
75
        // Update CSP header.
76
        if (array_key_exists('Content-Security-Policy', $headersToSend)) {
77
            $header = 'Content-Security-Policy';
78
            $headerValue = $headersToSend['Content-Security-Policy'];
79
            // Set report only mode if appropriate.
80
            if ($this->isCSPReportingOnly($request)) {
81
                unset($headersToSend['Content-Security-Policy']);
82
                $header = 'Content-Security-Policy-Report-Only';
83
            }
84
            // Update CSP header value.
85
            $headersToSend[$header] = $this->updateCspHeader($headerValue);
86
        }
87
        $this->extend('updateHeaders', $headersToSend, $request);
88
89
        // Add headers to response.
90
        foreach ($headersToSend as $header => $value) {
91
            if (empty($value)) {
92
                continue;
93
            }
94
            $value = preg_replace('/\v/', '', $value);
95
            $this->extend('updateHeader', $header, $value, $request);
96
            if ($value) {
97
                $response->addHeader($header, $value);
98
            }
99
        }
100
101
        return $response;
102
    }
103
104
    /**
105
     * Returns true if the Content-Security-Policy-Report-Only header should be used.
106
     * @return boolean
107
     */
108
    public function isCSPReportingOnly($request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

108
    public function isCSPReportingOnly(/** @scrutinizer ignore-unused */ $request)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
109
    {
110
        return SiteConfig::current_site_config()->CSPReportingOnly;
111
    }
112
113
    protected function getReportURI()
114
    {
115
        return Director::absoluteURL($this->config()->get('report_uri'));
116
    }
117
118
    protected function getIncludeSubdomains()
119
    {
120
        return $this->config()->get('report_to_subdomains');
121
    }
122
123
    protected function getReportToGroup()
124
    {
125
        return $this->config()->get('report_to_group');
126
    }
127
128
    protected function getReportURIDirective()
129
    {
130
        return "report-uri {$this->getReportURI()}";
131
    }
132
133
    protected function getReportToDirective()
134
    {
135
        return "report-to {$this->getReportToGroup()}";
136
    }
137
138
    protected function addReportToHeader(&$headers)
139
    {
140
        if (array_key_exists('Report-To', $headers)) {
141
            $headers['Report-To'] = $headers['Report-To'] . ',' . $this->getReportToHeader();
142
        } else {
143
            $headers['Report-To'] = $this->getReportToHeader();
144
        }
145
    }
146
147
    protected function getReportToHeader()
148
    {
149
        $header = [
150
            'group' => $this->getReportToGroup(),
151
            'max_age' => 1800,
152
            'endpoints' => [[
153
                'url' => $this->getReportURI(),
154
            ],],
155
            'include_subdomains' => $this->getIncludeSubdomains(),
156
        ];
157
        return json_encode($header);
158
    }
159
160
    protected function updateCspHeader($cspHeader)
161
    {
162
        if ($this->config()->get('enable_reporting')) {
163
            // Add or update report-uri directive.
164
            if (strpos($cspHeader, 'report-uri')) {
165
                $cspHeader = str_replace('report-uri', $this->getReportURIDirective(), $cspHeader);
166
            } else {
167
                $cspHeader = rtrim($cspHeader, ';') . "; {$this->getReportURIDirective()};";
168
            }
169
170
            // Add report-to directive.
171
            // Note that unlike report-uri, only the first endpoint is used if multiple are declared.
172
            if ($this->config()->get('use_report_to')) {
173
                if (strpos($cspHeader, 'report-to') === false) {
174
                    $cspHeader = rtrim($cspHeader, ';') . "; {$this->getReportToDirective()};";
175
                }
176
            }
177
        }
178
179
        return $cspHeader;
180
    }
181
}
182