Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
81.16% |
56 / 69 |
|
42.86% |
3 / 7 |
CRAP | |
0.00% |
0 / 1 |
CspHeaderGenerator | |
81.16% |
56 / 69 |
|
42.86% |
3 / 7 |
31.88 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getHeaders | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCspHeader | |
80.00% |
12 / 15 |
|
0.00% |
0 / 1 |
9.65 | |||
createHeaderObject | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getReportToHeader | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
5 | |||
getNetworkErrorLoggingHeader | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
4.00 |
1 | <?php |
2 | |
3 | /** |
4 | * Class CspHeaderGenerator |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Moravian Library 2019. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Security |
25 | * @author Josef Moravec <moravec@mzk.cz> |
26 | * @license https://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org Main Page |
28 | */ |
29 | |
30 | namespace VuFind\Security; |
31 | |
32 | use Laminas\Http\Header\ContentSecurityPolicy; |
33 | use Laminas\Http\Header\ContentSecurityPolicyReportOnly; |
34 | use Laminas\Http\Header\GenericHeader; |
35 | |
36 | use function in_array; |
37 | |
38 | /** |
39 | * VuFind class for generating Content Security Policy http headers. |
40 | * Also generates related headers like NEL (network error logging) |
41 | * and reporting headers like Report-To. |
42 | * |
43 | * @category VuFind |
44 | * @package Security |
45 | * @author Josef Moravec <moravec@mzk.cz> |
46 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
47 | * @link https://vufind.org/wiki/ Wiki |
48 | * |
49 | * @SuppressWarnings(PHPMD.NumberOfChildren) |
50 | */ |
51 | class CspHeaderGenerator implements |
52 | \Laminas\Log\LoggerAwareInterface |
53 | { |
54 | use \VuFind\Log\LoggerAwareTrait; |
55 | |
56 | /** |
57 | * Configuration for generator from contensecuritypolicy.ini |
58 | * |
59 | * @var \Laminas\Config\Config |
60 | */ |
61 | protected $config; |
62 | |
63 | /** |
64 | * Generated nonce used for one request |
65 | * |
66 | * @var string |
67 | */ |
68 | protected $nonce; |
69 | |
70 | /** |
71 | * List of directives that can work with nonce |
72 | * |
73 | * @var string[] |
74 | */ |
75 | protected $scriptDirectives = ['script-src', 'script-src-elem']; |
76 | |
77 | /** |
78 | * CspHeaderGenerator constructor. |
79 | * |
80 | * @param \Laminas\Config\Config $config Configuration |
81 | * @param \VuFind\Security\NonceGenerator $nonceGenerator Nonce generator |
82 | */ |
83 | public function __construct($config, $nonceGenerator) |
84 | { |
85 | $this->nonce = $nonceGenerator->getNonce(); |
86 | $this->config = $config; |
87 | } |
88 | |
89 | /** |
90 | * Create all relevant CSP-related headers based on given configuration |
91 | * |
92 | * @return array |
93 | */ |
94 | public function getHeaders() |
95 | { |
96 | $headers = []; |
97 | if ($cspHeader = $this->getCspHeader()) { |
98 | $headers[] = $cspHeader; |
99 | } |
100 | if ($reportToHeader = $this->getReportToHeader()) { |
101 | $headers[] = $reportToHeader; |
102 | } |
103 | if ($nelHeader = $this->getNetworkErrorLoggingHeader()) { |
104 | $headers[] = $nelHeader; |
105 | } |
106 | return $headers; |
107 | } |
108 | |
109 | /** |
110 | * Create CSP header base on given configuration |
111 | * |
112 | * @return ContentSecurityPolicy |
113 | * |
114 | * @deprecated Use getCspHeader instead |
115 | */ |
116 | public function getHeader() |
117 | { |
118 | return $this->getCspHeader(); |
119 | } |
120 | |
121 | /** |
122 | * Create CSP header base on given configuration |
123 | * |
124 | * @return ContentSecurityPolicy |
125 | */ |
126 | public function getCspHeader() |
127 | { |
128 | $cspHeader = $this->createHeaderObject(); |
129 | $directives = $this->config->Directives ?? []; |
130 | if (!$cspHeader || !$directives) { |
131 | return null; |
132 | } |
133 | foreach ($directives as $name => $value) { |
134 | $sources = $value->toArray(); |
135 | if ( |
136 | in_array($name, $this->scriptDirectives) |
137 | && $this->config->CSP->use_nonce |
138 | ) { |
139 | $sources[] = "'nonce-$this->nonce'"; |
140 | } |
141 | // Warn about report-to being used in place of report-uri |
142 | if ($name == 'report-to') { |
143 | foreach ($sources as $source) { |
144 | if (str_contains($source, '://')) { |
145 | $this->logWarning('CSP report-to directive should not be a URI.'); |
146 | } |
147 | } |
148 | } |
149 | $cspHeader->setDirective($name, $sources); |
150 | } |
151 | return $cspHeader; |
152 | } |
153 | |
154 | /** |
155 | * Create header object |
156 | * |
157 | * @return ContentSecurityPolicy |
158 | */ |
159 | protected function createHeaderObject() |
160 | { |
161 | $mode = $this->config->CSP->enabled[APPLICATION_ENV] ?? 'report_only'; |
162 | if (!$mode) { |
163 | return null; |
164 | } |
165 | return ('report_only' === $mode) |
166 | ? new ContentSecurityPolicyReportOnly() |
167 | : new ContentSecurityPolicy(); |
168 | } |
169 | |
170 | /** |
171 | * Create Report-To header based on given configuration |
172 | * |
173 | * @return ?GenericHeader |
174 | */ |
175 | public function getReportToHeader() |
176 | { |
177 | $reportToHeader = new GenericHeader(); |
178 | $reportToHeader->setFieldName('Report-To'); |
179 | $groupsText = []; |
180 | |
181 | $reportTo = $this->config->ReportTo; |
182 | foreach ($reportTo['groups'] ?? [] as $groupName) { |
183 | $configSectionName = 'ReportTo' . $groupName; |
184 | $groupConfig = $this->config->$configSectionName ?? false; |
185 | if ($groupConfig) { |
186 | $group = [ |
187 | 'group' => $groupName, |
188 | 'max_age' => $groupConfig->max_age ?? 86400, // one day |
189 | 'endpoints' => [], |
190 | ]; |
191 | foreach ($groupConfig->endpoints_url ?? [] as $url) { |
192 | $group['endpoints'][] = [ |
193 | 'url' => $url, |
194 | ]; |
195 | } |
196 | $groupsText[] = json_encode($group, JSON_UNESCAPED_SLASHES); |
197 | } |
198 | } |
199 | |
200 | if (!$groupsText) { |
201 | return null; |
202 | } |
203 | $reportToHeader->setFieldValue(implode(', ', $groupsText)); |
204 | return $reportToHeader; |
205 | } |
206 | |
207 | /** |
208 | * Create NEL (Network Error Logging) header based on given configuration |
209 | * |
210 | * @return ?GenericHeader |
211 | */ |
212 | public function getNetworkErrorLoggingHeader() |
213 | { |
214 | $nelHeader = new \Laminas\Http\Header\GenericHeader(); |
215 | $nelHeader->setFieldName('NEL'); |
216 | $nelData = []; |
217 | |
218 | $nelConfig = $this->config->NetworkErrorLogging; |
219 | if ($reportTo = $nelConfig['report_to'] ?? null) { |
220 | $nelData['report_to'] = $reportTo; |
221 | } else { |
222 | return null; |
223 | } |
224 | $nelData['max_age'] = $nelConfig['max_age'] ?? 86400; // one day |
225 | if (isset($nelConfig['include_subdomains'])) { |
226 | $nelData['include_subdomains'] = (bool)$nelConfig['include_subdomains']; |
227 | } |
228 | if (isset($nelConfig['failure_fraction'])) { |
229 | $nelData['failure_fraction'] = (float)$nelConfig['failure_fraction']; |
230 | } |
231 | |
232 | $nelText = json_encode($nelData, JSON_UNESCAPED_SLASHES); |
233 | $nelHeader->setFieldValue($nelText); |
234 | return $nelHeader; |
235 | } |
236 | } |