67 $this->hookRunner =
new HookRunner( $hookContainer );
82 $this->response->header(
83 "$headerName: $policy"
98 $cspConfig = $this->mwConfig->get(
'CSPHeader' );
99 $cspConfigReportOnly = $this->mwConfig->get(
'CSPReportOnlyHeader' );
102 $this->
sendCSPHeader( $cspConfigReportOnly, self::REPORT_ONLY_MODE );
119 if ( $reportOnly === self::REPORT_ONLY_MODE ) {
120 return 'Content-Security-Policy-Report-Only';
123 if ( $reportOnly === self::FULL_MODE ) {
124 return 'Content-Security-Policy';
126 throw new UnexpectedValueException(
"Mode '$reportOnly' not recognised" );
138 if ( $policyConfig ===
false ) {
142 if ( $policyConfig ===
true ) {
150 self::isNonceRequiredArray( [ $policyConfig ] )
154 throw new LogicException(
"Nonce requirement mismatch" );
165 $defaultSrc = [
'*',
'data:',
'blob:' ];
168 $scriptSrc = [
"'unsafe-eval'",
"blob:",
"'self'" ];
169 if ( $policyConfig[
'useNonces'] ??
true ) {
170 $scriptSrc[] =
"'nonce-" . $this->
getNonce() .
"'";
173 $scriptSrc = array_merge( $scriptSrc, $additionalSelfUrlsScript );
174 if ( isset( $policyConfig[
'script-src'] )
175 && is_array( $policyConfig[
'script-src'] )
177 foreach ( $policyConfig[
'script-src'] as $src ) {
182 if ( $policyConfig[
'unsafeFallback'] ??
true ) {
188 $scriptSrc[] =
"'unsafe-inline'";
194 if ( isset( $policyConfig[
'default-src'] )
195 && $policyConfig[
'default-src'] !==
false
197 $defaultSrc = array_merge(
198 [
"'self'",
'data:',
'blob:' ],
201 if ( is_array( $policyConfig[
'default-src'] ) ) {
202 foreach ( $policyConfig[
'default-src'] as $src ) {
208 if ( $policyConfig[
'includeCORS'] ??
true ) {
210 if ( !in_array(
'*', $defaultSrc ) ) {
211 $defaultSrc = array_merge( $defaultSrc, $CORSUrls );
215 if ( !in_array(
'*', $scriptSrc ) ) {
216 $scriptSrc = array_merge( $scriptSrc, $CORSUrls );
220 $defaultSrc = array_merge( $defaultSrc, $this->extraDefaultSrc );
221 $scriptSrc = array_merge( $scriptSrc, $this->extraScriptSrc );
223 $cssSrc = array_merge( $defaultSrc, $this->extraStyleSrc, [
"'unsafe-inline'" ] );
225 $this->hookRunner->onContentSecurityPolicyDefaultSource( $defaultSrc, $policyConfig, $mode );
226 $this->hookRunner->onContentSecurityPolicyScriptSource( $scriptSrc, $policyConfig, $mode );
228 if ( isset( $policyConfig[
'report-uri'] ) && $policyConfig[
'report-uri'] !==
true ) {
229 if ( $policyConfig[
'report-uri'] ===
false ) {
239 if ( !is_array( $defaultSrc )
240 || !in_array(
'*', $defaultSrc )
241 || !in_array(
'data:', $defaultSrc )
242 || !in_array(
'blob:', $defaultSrc )
253 $imgSrc = [
'*',
'data:',
'blob:' ];
255 $whitelist =
wfMessage(
'external_image_whitelist' )
256 ->inContentLanguage()
258 if ( preg_match(
'/^\s*[^\s#]/m', $whitelist ) ) {
259 $imgSrc = [
'*',
'data:',
'blob:' ];
265 if ( !isset( $policyConfig[
'object-src'] ) || $policyConfig[
'object-src'] ===
true ) {
266 $objectSrc = [
"'none'" ];
268 $objectSrc = (array)( $policyConfig[
'object-src'] ?: [] );
270 $objectSrc = array_map( [ $this,
'escapeUrlForCSP' ], $objectSrc );
274 $directives[] =
'script-src ' . implode(
' ', array_unique( $scriptSrc ) );
277 $directives[] =
'default-src ' . implode(
' ', array_unique( $defaultSrc ) );
280 $directives[] =
'style-src ' . implode(
' ', array_unique( $cssSrc ) );
283 $directives[] =
'img-src ' . implode(
' ', array_unique( $imgSrc ) );
286 $directives[] =
'object-src ' . implode(
' ', $objectSrc );
289 $directives[] =
'report-uri ' . $reportUri;
292 $this->hookRunner->onContentSecurityPolicyDirectives( $directives, $policyConfig, $mode );
294 return implode(
'; ', $directives );
306 'action' =>
'cspreport',
309 if ( $mode === self::REPORT_ONLY_MODE ) {
310 $apiArguments[
'reportonly'] =
'1';
336 if ( preg_match(
'/^[a-z][a-z0-9+.-]*:$/i', $url ) ) {
341 if ( !$bits && strpos( $url,
'/' ) ===
false ) {
347 if ( $bits && isset( $bits[
'host'] )
348 && $bits[
'host'] !== $this->mwConfig->get(
'ServerName' )
350 $result = $bits[
'host'];
351 if ( $bits[
'scheme'] !==
'' ) {
352 $result = $bits[
'scheme'] . $bits[
'delimiter'] . $result;
354 if ( isset( $bits[
'port'] ) ) {
355 $result .=
':' . $bits[
'port'];
366 $additionalUrls = [];
368 $pathVars = [
'LoadScript',
'ExtensionAssetsPath',
'ResourceBasePath' ];
370 foreach ( $pathVars as
$path ) {
371 $url = $this->mwConfig->get(
$path );
373 if ( $preparedUrl ) {
374 $additionalUrls[] = $preparedUrl;
377 $RLSources = $this->mwConfig->get(
'ResourceLoaderSources' );
378 foreach ( $RLSources as $wiki => $sources ) {
379 foreach ( $sources as $id => $value ) {
382 $additionalUrls[] = $url;
387 return array_unique( $additionalUrls );
402 $additionalSelfUrls = [];
409 $callback =
static function ( $repo, &$urls ) {
410 $urls[] = $repo->getZoneUrl(
'public' );
411 $urls[] = $repo->getZoneUrl(
'transcoded' );
412 $urls[] = $repo->getZoneUrl(
'thumb' );
413 $urls[] = $repo->getDescriptionStylesheetUrl();
415 $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
416 $localRepo = $repoGroup->getRepo(
'local' );
417 $callback( $localRepo, $pathUrls );
418 $repoGroup->forEachForeignRepo( $callback, [ &$pathUrls ] );
421 $pathGlobals = [
'LoadScript',
'ExtensionAssetsPath',
'StylePath',
'ResourceBasePath' ];
422 foreach ( $pathGlobals as
$path ) {
423 $pathUrls[] = $this->mwConfig->get(
$path );
425 foreach ( $pathUrls as
$path ) {
427 if ( $preparedUrl !==
false ) {
428 $additionalSelfUrls[] = $preparedUrl;
431 $RLSources = $this->mwConfig->get(
'ResourceLoaderSources' );
433 foreach ( $RLSources as $wiki => $sources ) {
434 foreach ( $sources as $id => $value ) {
437 $additionalSelfUrls[] = $url;
442 return array_unique( $additionalSelfUrls );
458 $additionalUrls = [];
459 $CORSSources = $this->mwConfig->get(
'CrossSiteAJAXdomains' );
460 foreach ( $CORSSources as
$source ) {
461 if ( strpos(
$source,
'?' ) !==
false ) {
467 $additionalUrls[] = $url;
470 return $additionalUrls;
499 return (
bool)preg_match(
'!Firefox/4[0-2]\.!', $ua );
510 $config->
get(
'CSPHeader' ),
511 $config->
get(
'CSPReportOnlyHeader' )
523 foreach ( $configs as $headerConfig ) {
525 $headerConfig ===
true ||
526 ( is_array( $headerConfig ) &&
527 !isset( $headerConfig[
'useNonces'] ) ) ||
528 ( is_array( $headerConfig ) &&
529 isset( $headerConfig[
'useNonces'] ) &&
530 $headerConfig[
'useNonces'] )
545 if ( !self::isNonceRequired( $this->mwConfig ) ) {
548 if ( $this->nonce ===
null ) {
549 $rand = random_bytes( 15 );
550 $this->nonce = base64_encode( $rand );
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
getAdditionalSelfUrls()
Get additional host names for the wiki (e.g.
static isNonceRequired(Config $config)
Should we set nonce attribute.
prepareUrlForCSP( $url)
Given a url, convert to form needed for CSP.
__construct(WebResponse $response, Config $mwConfig, HookContainer $hookContainer)
addScriptSrc( $source)
So for example, if an extension added a special page that loaded something it might call $this->getOu...
makeCSPDirectives( $policyConfig, $mode)
Determine what CSP policies to set for this page.
sendHeaders()
Send CSP headers based on wiki config.
static falsePositiveBrowser( $ua)
Does this browser give false positive reports?
getHeaderName( $reportOnly)
escapeUrlForCSP( $url)
CSP spec says ',' and ';' are not allowed to appear in urls.
addStyleSrc( $source)
So for example, if an extension added a special page that loaded external CSS it might call $this->ge...
string $nonce
The nonce to use for inline scripts (from OutputPage)
getReportUri( $mode)
Get the default report uri.
addDefaultSrc( $source)
If possible you should use a more specific source type then default.
getCORSSources()
include domains that are allowed to send us CORS requests.
static isNonceRequiredArray(array $configs)
Does a specific config require a nonce.
getAdditionalSelfUrlsScript()
sendCSPHeader( $csp, $reportOnly)
Send a single CSP header based on a given policy config.
getNonce()
Get the nonce if nonce is in use.
Config $mwConfig
The site configuration object.
Allow programs to request this object from WebRequest::response() and handle all outputting (or lack ...
Interface for configuration instances.
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".