Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
74.63% |
150 / 201 |
|
55.56% |
10 / 18 |
CRAP | |
0.00% |
0 / 1 |
ContentSecurityPolicy | |
75.00% |
150 / 200 |
|
55.56% |
10 / 18 |
190.64 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getDirectives | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
sendHeaders | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getHeaderName | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
makeCSPDirectives | |
89.61% |
69 / 77 |
|
0.00% |
0 / 1 |
37.45 | |||
getReportUri | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
prepareUrlForCSP | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
9 | |||
getAdditionalSelfUrlsScript | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
getAdditionalSelfUrls | |
87.10% |
27 / 31 |
|
0.00% |
0 / 1 |
7.11 | |||
getCORSSources | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
escapeUrlForCSP | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
falsePositiveBrowser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isNonceRequired | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
isNonceRequiredArray | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
30 | |||
getNonce | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addDefaultSrc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addStyleSrc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addScriptSrc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Request; |
22 | |
23 | use MediaWiki\Config\Config; |
24 | use MediaWiki\HookContainer\HookContainer; |
25 | use MediaWiki\HookContainer\HookRunner; |
26 | use MediaWiki\MainConfigNames; |
27 | use MediaWiki\MediaWikiServices; |
28 | use UnexpectedValueException; |
29 | |
30 | /** |
31 | * Handle sending Content-Security-Policy headers |
32 | * |
33 | * @author Copyright 2015–2018 Brian Wolff |
34 | * @see https://www.w3.org/TR/CSP2/ |
35 | * @since 1.32 |
36 | */ |
37 | class ContentSecurityPolicy { |
38 | public const REPORT_ONLY_MODE = 1; |
39 | public const FULL_MODE = 2; |
40 | |
41 | /** @var Config The site configuration object */ |
42 | private $mwConfig; |
43 | /** @var WebResponse */ |
44 | private $response; |
45 | |
46 | /** @var string[] */ |
47 | private $extraDefaultSrc = []; |
48 | /** @var string[] */ |
49 | private $extraScriptSrc = []; |
50 | /** @var string[] */ |
51 | private $extraStyleSrc = []; |
52 | |
53 | /** @var HookRunner */ |
54 | private $hookRunner; |
55 | |
56 | /** |
57 | * @note As a general rule, you would not construct this class directly |
58 | * but use the instance from OutputPage::getCSP() |
59 | * @internal |
60 | * @param WebResponse $response |
61 | * @param Config $mwConfig |
62 | * @param HookContainer $hookContainer |
63 | * @since 1.35 Method signature changed |
64 | */ |
65 | public function __construct( |
66 | WebResponse $response, |
67 | Config $mwConfig, |
68 | HookContainer $hookContainer |
69 | ) { |
70 | $this->response = $response; |
71 | $this->mwConfig = $mwConfig; |
72 | $this->hookRunner = new HookRunner( $hookContainer ); |
73 | } |
74 | |
75 | /** |
76 | * Get the CSP directives for the wiki. |
77 | * @return string[] Array of CSP directives (header name => header value). The array keys will be |
78 | * ContentSecurityPolicy::FULL_MODE and ContentSecurityPolicy::REPORT_ONLY_MODE; they might not |
79 | * be present if the wiki is configured no to use the given type of CSP. |
80 | * @phan-return array{Content-Security-Policy?:string,Content-Security-Policy-Report-Only?:string} |
81 | * @since 1.42 |
82 | */ |
83 | public function getDirectives() { |
84 | $cspConfig = $this->mwConfig->get( MainConfigNames::CSPHeader ); |
85 | $cspConfigReportOnly = $this->mwConfig->get( MainConfigNames::CSPReportOnlyHeader ); |
86 | |
87 | $cspPolicy = $this->makeCSPDirectives( $cspConfig, self::FULL_MODE ); |
88 | $cspReportOnlyPolicy = $this->makeCSPDirectives( $cspConfigReportOnly, self::REPORT_ONLY_MODE ); |
89 | |
90 | return array_filter( [ |
91 | $this->getHeaderName( self::FULL_MODE ) => $cspPolicy, |
92 | $this->getHeaderName( self::REPORT_ONLY_MODE ) => $cspReportOnlyPolicy, |
93 | ] ); |
94 | } |
95 | |
96 | /** |
97 | * Send CSP headers based on wiki config |
98 | * |
99 | * Main method that callers (OutputPage) are expected to use. |
100 | * As a general rule, you would never call this in an extension unless |
101 | * you have disabled OutputPage and are fully controlling the output. |
102 | * |
103 | * @since 1.35 |
104 | */ |
105 | public function sendHeaders() { |
106 | $directives = $this->getDirectives(); |
107 | foreach ( $directives as $headerName => $policy ) { |
108 | $this->response->header( "$headerName: $policy" ); |
109 | } |
110 | |
111 | // This used to insert a <meta> tag here, per advice at |
112 | // https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/ |
113 | // The goal was to prevent nonce from working after the page hit onready, |
114 | // This would help in old browsers that didn't support nonces, and |
115 | // also assist for Varnish-cached pages which repeat nonces. |
116 | // However, this is incompatible with how ResourceLoader runs code |
117 | // from mw.loader.store, so it was removed. |
118 | } |
119 | |
120 | /** |
121 | * @param int $reportOnly Either self::REPORT_ONLY_MODE or self::FULL_MODE |
122 | * @return string Name of http header |
123 | * @throws UnexpectedValueException |
124 | */ |
125 | private function getHeaderName( $reportOnly ) { |
126 | if ( $reportOnly === self::REPORT_ONLY_MODE ) { |
127 | return 'Content-Security-Policy-Report-Only'; |
128 | } |
129 | |
130 | if ( $reportOnly === self::FULL_MODE ) { |
131 | return 'Content-Security-Policy'; |
132 | } |
133 | throw new UnexpectedValueException( "Mode '$reportOnly' not recognised" ); |
134 | } |
135 | |
136 | /** |
137 | * Determine what CSP policies to set for this page |
138 | * |
139 | * @param array|bool $policyConfig Policy configuration |
140 | * (Either $wgCSPHeader or $wgCSPReportOnlyHeader) |
141 | * @param int $mode self::REPORT_ONLY_MODE, self::FULL_MODE |
142 | * @return string Policy directives, or empty string for no policy. |
143 | */ |
144 | private function makeCSPDirectives( $policyConfig, $mode ) { |
145 | if ( $policyConfig === false ) { |
146 | // CSP is disabled |
147 | return ''; |
148 | } |
149 | if ( $policyConfig === true ) { |
150 | $policyConfig = []; |
151 | } |
152 | |
153 | $mwConfig = $this->mwConfig; |
154 | |
155 | if ( |
156 | self::isNonceRequired( $mwConfig ) || |
157 | self::isNonceRequiredArray( [ $policyConfig ] ) |
158 | ) { |
159 | wfDeprecated( 'wgCSPHeader "useNonces" option', '1.41' ); |
160 | } |
161 | |
162 | $additionalSelfUrls = $this->getAdditionalSelfUrls(); |
163 | $additionalSelfUrlsScript = $this->getAdditionalSelfUrlsScript(); |
164 | |
165 | // If no default-src is sent at all, it seems browsers (or at least some), |
166 | // interpret that as allow anything, but the spec seems to imply that |
167 | // "data:" and "blob:" should be blocked. |
168 | $defaultSrc = [ '*', 'data:', 'blob:' ]; |
169 | |
170 | $imgSrc = false; |
171 | $scriptSrc = [ "'unsafe-eval'", "blob:", "'self'" ]; |
172 | |
173 | $scriptSrc = array_merge( $scriptSrc, $additionalSelfUrlsScript ); |
174 | if ( isset( $policyConfig['script-src'] ) |
175 | && is_array( $policyConfig['script-src'] ) |
176 | ) { |
177 | foreach ( $policyConfig['script-src'] as $src ) { |
178 | $scriptSrc[] = $this->escapeUrlForCSP( $src ); |
179 | } |
180 | } |
181 | // Note: default on if unspecified. |
182 | if ( $policyConfig['unsafeFallback'] ?? true ) { |
183 | // unsafe-inline should be ignored on browsers that support 'nonce-foo' sources. |
184 | // Some older versions of firefox don't follow this rule, but new browsers do. |
185 | // (Should be for at least Firefox 40+). |
186 | $scriptSrc[] = "'unsafe-inline'"; |
187 | } |
188 | // If default source option set to true or an array of urls, |
189 | // set a restrictive default-src. |
190 | // If set to false, we send a lenient default-src, |
191 | // see the code above where $defaultSrc is set initially. |
192 | if ( isset( $policyConfig['default-src'] ) |
193 | && $policyConfig['default-src'] !== false |
194 | ) { |
195 | $defaultSrc = array_merge( |
196 | [ "'self'", 'data:', 'blob:' ], |
197 | $additionalSelfUrls |
198 | ); |
199 | if ( is_array( $policyConfig['default-src'] ) ) { |
200 | foreach ( $policyConfig['default-src'] as $src ) { |
201 | $defaultSrc[] = $this->escapeUrlForCSP( $src ); |
202 | } |
203 | } |
204 | } |
205 | |
206 | if ( $policyConfig['includeCORS'] ?? true ) { |
207 | $CORSUrls = $this->getCORSSources(); |
208 | if ( !in_array( '*', $defaultSrc ) ) { |
209 | $defaultSrc = array_merge( $defaultSrc, $CORSUrls ); |
210 | } |
211 | // Unlikely to have * in scriptSrc, but doesn't |
212 | // hurt to check. |
213 | if ( !in_array( '*', $scriptSrc ) ) { |
214 | $scriptSrc = array_merge( $scriptSrc, $CORSUrls ); |
215 | } |
216 | } |
217 | |
218 | $defaultSrc = array_merge( $defaultSrc, $this->extraDefaultSrc ); |
219 | $scriptSrc = array_merge( $scriptSrc, $this->extraScriptSrc ); |
220 | |
221 | $cssSrc = array_merge( $defaultSrc, $this->extraStyleSrc, [ "'unsafe-inline'" ] ); |
222 | |
223 | $this->hookRunner->onContentSecurityPolicyDefaultSource( $defaultSrc, $policyConfig, $mode ); |
224 | $this->hookRunner->onContentSecurityPolicyScriptSource( $scriptSrc, $policyConfig, $mode ); |
225 | |
226 | if ( isset( $policyConfig['report-uri'] ) && $policyConfig['report-uri'] !== true ) { |
227 | if ( $policyConfig['report-uri'] === false ) { |
228 | $reportUri = false; |
229 | } else { |
230 | $reportUri = $this->escapeUrlForCSP( $policyConfig['report-uri'] ); |
231 | } |
232 | } else { |
233 | $reportUri = $this->getReportUri( $mode ); |
234 | } |
235 | |
236 | // Only send an img-src, if we're sending a restrictive default. |
237 | if ( !is_array( $defaultSrc ) |
238 | || !in_array( '*', $defaultSrc ) |
239 | || !in_array( 'data:', $defaultSrc ) |
240 | || !in_array( 'blob:', $defaultSrc ) |
241 | ) { |
242 | // A future todo might be to make the allow options only |
243 | // add all the allowed sites to the header, instead of |
244 | // allowing all (Assuming there is a small number of sites). |
245 | // For now, the external image feature disables the limits |
246 | // CSP puts on external images. |
247 | if ( $mwConfig->get( MainConfigNames::AllowExternalImages ) |
248 | || $mwConfig->get( MainConfigNames::AllowExternalImagesFrom ) |
249 | ) { |
250 | $imgSrc = [ '*', 'data:', 'blob:' ]; |
251 | } elseif ( $mwConfig->get( MainConfigNames::EnableImageWhitelist ) ) { |
252 | $whitelist = wfMessage( 'external_image_whitelist' ) |
253 | ->inContentLanguage() |
254 | ->plain(); |
255 | if ( preg_match( '/^\s*[^\s#]/m', $whitelist ) ) { |
256 | $imgSrc = [ '*', 'data:', 'blob:' ]; |
257 | } |
258 | } |
259 | } |
260 | // Default value 'none'. true is none, false is nothing, string is single directive, |
261 | // array is list. |
262 | if ( !isset( $policyConfig['object-src'] ) || $policyConfig['object-src'] === true ) { |
263 | $objectSrc = [ "'none'" ]; |
264 | } else { |
265 | $objectSrc = (array)( $policyConfig['object-src'] ?: [] ); |
266 | } |
267 | $objectSrc = array_map( [ $this, 'escapeUrlForCSP' ], $objectSrc ); |
268 | |
269 | $directives = []; |
270 | if ( $scriptSrc ) { |
271 | $directives[] = 'script-src ' . implode( ' ', array_unique( $scriptSrc ) ); |
272 | } |
273 | if ( $defaultSrc ) { |
274 | $directives[] = 'default-src ' . implode( ' ', array_unique( $defaultSrc ) ); |
275 | } |
276 | if ( $cssSrc ) { |
277 | $directives[] = 'style-src ' . implode( ' ', array_unique( $cssSrc ) ); |
278 | } |
279 | if ( $imgSrc ) { |
280 | $directives[] = 'img-src ' . implode( ' ', array_unique( $imgSrc ) ); |
281 | } |
282 | if ( $objectSrc ) { |
283 | $directives[] = 'object-src ' . implode( ' ', $objectSrc ); |
284 | } |
285 | if ( $reportUri ) { |
286 | $directives[] = 'report-uri ' . $reportUri; |
287 | } |
288 | |
289 | $this->hookRunner->onContentSecurityPolicyDirectives( $directives, $policyConfig, $mode ); |
290 | |
291 | return implode( '; ', $directives ); |
292 | } |
293 | |
294 | /** |
295 | * Get the default report uri. |
296 | * |
297 | * @param int $mode self::*_MODE constant. |
298 | * @return string The URI to send reports to. |
299 | * @throws UnexpectedValueException if given invalid mode. |
300 | */ |
301 | private function getReportUri( $mode ) { |
302 | $apiArguments = [ |
303 | 'action' => 'cspreport', |
304 | 'format' => 'json' |
305 | ]; |
306 | if ( $mode === self::REPORT_ONLY_MODE ) { |
307 | $apiArguments['reportonly'] = '1'; |
308 | } |
309 | $reportUri = wfAppendQuery( wfScript( 'api' ), $apiArguments ); |
310 | |
311 | // Per spec, ';' and ',' must be hex-escaped in report URI |
312 | $reportUri = $this->escapeUrlForCSP( $reportUri ); |
313 | return $reportUri; |
314 | } |
315 | |
316 | /** |
317 | * Given a url, convert to form needed for CSP. |
318 | * |
319 | * Currently this does either scheme + host, or |
320 | * if protocol relative, just the host. Future versions |
321 | * could potentially preserve some of the path, if its determined |
322 | * that that would be a good idea. |
323 | * |
324 | * @note This does the extra escaping for CSP, but assumes the url |
325 | * has already had normal url escaping applied. |
326 | * @note This discards urls same as server name, as 'self' directive |
327 | * takes care of that. |
328 | * @param string $url |
329 | * @return string|bool Converted url or false on failure |
330 | */ |
331 | private function prepareUrlForCSP( $url ) { |
332 | $result = false; |
333 | if ( preg_match( '/^[a-z][a-z0-9+.-]*:$/i', $url ) ) { |
334 | // A schema source (e.g. blob: or data:) |
335 | return $url; |
336 | } |
337 | $bits = wfGetUrlUtils()->parse( $url ); |
338 | if ( !$bits && strpos( $url, '/' ) === false ) { |
339 | // probably something like example.com. |
340 | // try again protocol-relative. |
341 | $url = '//' . $url; |
342 | $bits = wfGetUrlUtils()->parse( $url ); |
343 | } |
344 | if ( $bits && isset( $bits['host'] ) |
345 | && $bits['host'] !== $this->mwConfig->get( MainConfigNames::ServerName ) |
346 | ) { |
347 | $result = $bits['host']; |
348 | if ( $bits['scheme'] !== '' ) { |
349 | $result = $bits['scheme'] . $bits['delimiter'] . $result; |
350 | } |
351 | if ( isset( $bits['port'] ) ) { |
352 | $result .= ':' . $bits['port']; |
353 | } |
354 | $result = $this->escapeUrlForCSP( $result ); |
355 | } |
356 | return $result; |
357 | } |
358 | |
359 | /** |
360 | * @return string[] Additional sources for loading scripts from |
361 | */ |
362 | private function getAdditionalSelfUrlsScript() { |
363 | $additionalUrls = []; |
364 | // wgExtensionAssetsPath for ?debug=true mode |
365 | $pathVars = [ |
366 | MainConfigNames::LoadScript, |
367 | MainConfigNames::ExtensionAssetsPath, |
368 | MainConfigNames::ResourceBasePath, |
369 | ]; |
370 | |
371 | foreach ( $pathVars as $path ) { |
372 | $url = $this->mwConfig->get( $path ); |
373 | $preparedUrl = $this->prepareUrlForCSP( $url ); |
374 | if ( $preparedUrl ) { |
375 | $additionalUrls[] = $preparedUrl; |
376 | } |
377 | } |
378 | $RLSources = $this->mwConfig->get( MainConfigNames::ResourceLoaderSources ); |
379 | foreach ( $RLSources as $sources ) { |
380 | foreach ( $sources as $value ) { |
381 | $url = $this->prepareUrlForCSP( $value ); |
382 | if ( $url ) { |
383 | $additionalUrls[] = $url; |
384 | } |
385 | } |
386 | } |
387 | |
388 | return array_unique( $additionalUrls ); |
389 | } |
390 | |
391 | /** |
392 | * Get additional host names for the wiki (e.g. if static content loaded elsewhere) |
393 | * |
394 | * @note These are general load sources, not script sources |
395 | * @return string[] Array of other urls for wiki (for use in default-src) |
396 | */ |
397 | private function getAdditionalSelfUrls() { |
398 | // XXX on a foreign repo, the included description page can have anything on it, |
399 | // including inline scripts. But nobody does that. |
400 | |
401 | // In principle, you can have even more complex configs... (e.g. The urlsByExt option) |
402 | $pathUrls = []; |
403 | $additionalSelfUrls = []; |
404 | |
405 | // Future todo: The zone urls should never go into |
406 | // style-src. They should either be only in img-src, or if |
407 | // img-src unspecified they should be in default-src. Similarly, |
408 | // the DescriptionStylesheetUrl only needs to be in style-src |
409 | // (or default-src if style-src unspecified). |
410 | $callback = static function ( $repo, &$urls ) { |
411 | $urls[] = $repo->getZoneUrl( 'public' ); |
412 | $urls[] = $repo->getZoneUrl( 'transcoded' ); |
413 | $urls[] = $repo->getZoneUrl( 'thumb' ); |
414 | $urls[] = $repo->getDescriptionStylesheetUrl(); |
415 | }; |
416 | $repoGroup = MediaWikiServices::getInstance()->getRepoGroup(); |
417 | $localRepo = $repoGroup->getRepo( 'local' ); |
418 | $callback( $localRepo, $pathUrls ); |
419 | $repoGroup->forEachForeignRepo( $callback, [ &$pathUrls ] ); |
420 | |
421 | // Globals that might point to a different domain |
422 | $pathGlobals = [ |
423 | MainConfigNames::LoadScript, |
424 | MainConfigNames::ExtensionAssetsPath, |
425 | MainConfigNames::StylePath, |
426 | MainConfigNames::ResourceBasePath, |
427 | ]; |
428 | foreach ( $pathGlobals as $path ) { |
429 | $pathUrls[] = $this->mwConfig->get( $path ); |
430 | } |
431 | foreach ( $pathUrls as $path ) { |
432 | $preparedUrl = $this->prepareUrlForCSP( $path ); |
433 | if ( $preparedUrl !== false ) { |
434 | $additionalSelfUrls[] = $preparedUrl; |
435 | } |
436 | } |
437 | $RLSources = $this->mwConfig->get( MainConfigNames::ResourceLoaderSources ); |
438 | |
439 | foreach ( $RLSources as $sources ) { |
440 | foreach ( $sources as $value ) { |
441 | $url = $this->prepareUrlForCSP( $value ); |
442 | if ( $url ) { |
443 | $additionalSelfUrls[] = $url; |
444 | } |
445 | } |
446 | } |
447 | |
448 | return array_unique( $additionalSelfUrls ); |
449 | } |
450 | |
451 | /** |
452 | * include domains that are allowed to send us CORS requests. |
453 | * |
454 | * Technically, $wgCrossSiteAJAXdomains lists things that are allowed to talk to us |
455 | * not things that we are allowed to talk to - but if something is allowed to talk to us, |
456 | * then there is a good chance that we should probably be allowed to talk to it. |
457 | * |
458 | * This is configurable with the 'includeCORS' key in the CSP config, and enabled |
459 | * by default. |
460 | * @note CORS domains with single character ('?') wildcards, are not included. |
461 | * @return array Additional hosts |
462 | */ |
463 | private function getCORSSources() { |
464 | $additionalUrls = []; |
465 | $CORSSources = $this->mwConfig->get( MainConfigNames::CrossSiteAJAXdomains ); |
466 | foreach ( $CORSSources as $source ) { |
467 | if ( strpos( $source, '?' ) !== false ) { |
468 | // CSP doesn't support single char wildcard |
469 | continue; |
470 | } |
471 | $url = $this->prepareUrlForCSP( $source ); |
472 | if ( $url ) { |
473 | $additionalUrls[] = $url; |
474 | } |
475 | } |
476 | return $additionalUrls; |
477 | } |
478 | |
479 | /** |
480 | * CSP spec says ',' and ';' are not allowed to appear in urls. |
481 | * |
482 | * @note This assumes that normal escaping has been applied to the url |
483 | * @param string $url URL (or possibly just part of one) |
484 | * @return string |
485 | */ |
486 | private function escapeUrlForCSP( $url ) { |
487 | return str_replace( |
488 | [ ';', ',' ], |
489 | [ '%3B', '%2C' ], |
490 | $url |
491 | ); |
492 | } |
493 | |
494 | /** |
495 | * Does this browser give false positive reports? |
496 | * |
497 | * Some versions of firefox (40-42) incorrectly report a CSP |
498 | * violation for nonce sources, despite allowing them. |
499 | * |
500 | * @see https://bugzilla.mozilla.org/show_bug.cgi?id=1026520 |
501 | * @param string $ua User-agent header |
502 | * @return bool |
503 | */ |
504 | public static function falsePositiveBrowser( $ua ) { |
505 | return (bool)preg_match( '!Firefox/4[0-2]\.!', $ua ); |
506 | } |
507 | |
508 | /** |
509 | * Should we set nonce attribute |
510 | * |
511 | * @param Config $config |
512 | * @return bool |
513 | */ |
514 | public static function isNonceRequired( Config $config ) { |
515 | $configs = [ |
516 | $config->get( MainConfigNames::CSPHeader ), |
517 | $config->get( MainConfigNames::CSPReportOnlyHeader ), |
518 | ]; |
519 | return self::isNonceRequiredArray( $configs ); |
520 | } |
521 | |
522 | /** |
523 | * Does a specific config require a nonce |
524 | * |
525 | * @param array $configs An array of CSP config arrays |
526 | * @return bool |
527 | */ |
528 | private static function isNonceRequiredArray( array $configs ) { |
529 | foreach ( $configs as $headerConfig ) { |
530 | if ( |
531 | is_array( $headerConfig ) && |
532 | isset( $headerConfig['useNonces'] ) && |
533 | $headerConfig['useNonces'] |
534 | ) { |
535 | return true; |
536 | } |
537 | } |
538 | return false; |
539 | } |
540 | |
541 | /** |
542 | * Get the nonce if nonce is in use |
543 | * |
544 | * Not currently supported or implemented. |
545 | * |
546 | * @since 1.35 |
547 | * @return false |
548 | */ |
549 | public function getNonce() { |
550 | return false; |
551 | } |
552 | |
553 | /** |
554 | * If possible you should use a more specific source type then default. |
555 | * |
556 | * So for example, if an extension added a special page that loaded something |
557 | * it might call $this->getOutput()->getCSP()->addDefaultSrc( '*.example.com' ); |
558 | * |
559 | * @since 1.35 |
560 | * @param string $source Source to add. |
561 | * e.g. blob:, *.example.com, %https://example.com, example.com/foo |
562 | */ |
563 | public function addDefaultSrc( $source ) { |
564 | $this->extraDefaultSrc[] = $this->prepareUrlForCSP( $source ); |
565 | } |
566 | |
567 | /** |
568 | * So for example, if an extension added a special page that loaded external CSS |
569 | * it might call $this->getOutput()->getCSP()->addStyleSrc( '*.example.com' ); |
570 | * |
571 | * @since 1.35 |
572 | * @param string $source Source to add. |
573 | * e.g. blob:, *.example.com, %https://example.com, example.com/foo |
574 | */ |
575 | public function addStyleSrc( $source ) { |
576 | $this->extraStyleSrc[] = $this->prepareUrlForCSP( $source ); |
577 | } |
578 | |
579 | /** |
580 | * So for example, if an extension added a special page that loaded something |
581 | * it might call $this->getOutput()->getCSP()->addScriptSrc( '*.example.com' ); |
582 | * |
583 | * @since 1.35 |
584 | * @warning Be careful including external scripts, as they can take over accounts. |
585 | * @param string $source Source to add. |
586 | * e.g. blob:, *.example.com, %https://example.com, example.com/foo |
587 | */ |
588 | public function addScriptSrc( $source ) { |
589 | $this->extraScriptSrc[] = $this->prepareUrlForCSP( $source ); |
590 | } |
591 | } |
592 | |
593 | /** @deprecated class alias since 1.40 */ |
594 | class_alias( ContentSecurityPolicy::class, 'ContentSecurityPolicy' ); |