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 );
121 if ( $reportOnly === self::REPORT_ONLY_MODE ) {
122 return 'Content-Security-Policy-Report-Only';
125 if ( $reportOnly === self::FULL_MODE ) {
126 return 'Content-Security-Policy';
128 throw new UnexpectedValueException(
"Mode '$reportOnly' not recognised" );
140 if ( $policyConfig ===
false ) {
144 if ( $policyConfig ===
true ) {
152 self::isNonceRequiredArray( [ $policyConfig ] )
156 throw new LogicException(
"Nonce requirement mismatch" );
167 $defaultSrc = [
'*',
'data:',
'blob:' ];
170 $scriptSrc = [
"'unsafe-eval'",
"blob:",
"'self'" ];
171 if ( $policyConfig[
'useNonces'] ??
true ) {
172 $scriptSrc[] =
"'nonce-" . $this->
getNonce() .
"'";
175 $scriptSrc = array_merge( $scriptSrc, $additionalSelfUrlsScript );
176 if ( isset( $policyConfig[
'script-src'] )
177 && is_array( $policyConfig[
'script-src'] )
179 foreach ( $policyConfig[
'script-src'] as $src ) {
184 if ( $policyConfig[
'unsafeFallback'] ??
true ) {
190 $scriptSrc[] =
"'unsafe-inline'";
196 if ( isset( $policyConfig[
'default-src'] )
197 && $policyConfig[
'default-src'] !==
false
199 $defaultSrc = array_merge(
200 [
"'self'",
'data:',
'blob:' ],
203 if ( is_array( $policyConfig[
'default-src'] ) ) {
204 foreach ( $policyConfig[
'default-src'] as $src ) {
210 if ( $policyConfig[
'includeCORS'] ??
true ) {
212 if ( !in_array(
'*', $defaultSrc ) ) {
213 $defaultSrc = array_merge( $defaultSrc, $CORSUrls );
217 if ( !in_array(
'*', $scriptSrc ) ) {
218 $scriptSrc = array_merge( $scriptSrc, $CORSUrls );
222 $defaultSrc = array_merge( $defaultSrc, $this->extraDefaultSrc );
223 $scriptSrc = array_merge( $scriptSrc, $this->extraScriptSrc );
225 $cssSrc = array_merge( $defaultSrc, $this->extraStyleSrc, [
"'unsafe-inline'" ] );
227 $this->hookRunner->onContentSecurityPolicyDefaultSource( $defaultSrc, $policyConfig, $mode );
228 $this->hookRunner->onContentSecurityPolicyScriptSource( $scriptSrc, $policyConfig, $mode );
230 if ( isset( $policyConfig[
'report-uri'] ) && $policyConfig[
'report-uri'] !==
true ) {
231 if ( $policyConfig[
'report-uri'] ===
false ) {
241 if ( !is_array( $defaultSrc )
242 || !in_array(
'*', $defaultSrc )
243 || !in_array(
'data:', $defaultSrc )
244 || !in_array(
'blob:', $defaultSrc )
255 $imgSrc = [
'*',
'data:',
'blob:' ];
257 $whitelist =
wfMessage(
'external_image_whitelist' )
258 ->inContentLanguage()
260 if ( preg_match(
'/^\s*[^\s#]/m', $whitelist ) ) {
261 $imgSrc = [
'*',
'data:',
'blob:' ];
267 if ( !isset( $policyConfig[
'object-src'] ) || $policyConfig[
'object-src'] ===
true ) {
268 $objectSrc = [
"'none'" ];
270 $objectSrc = (array)( $policyConfig[
'object-src'] ?: [] );
272 $objectSrc = array_map( [ $this,
'escapeUrlForCSP' ], $objectSrc );
276 $directives[] =
'script-src ' . implode(
' ', array_unique( $scriptSrc ) );
279 $directives[] =
'default-src ' . implode(
' ', array_unique( $defaultSrc ) );
282 $directives[] =
'style-src ' . implode(
' ', array_unique( $cssSrc ) );
285 $directives[] =
'img-src ' . implode(
' ', array_unique( $imgSrc ) );
288 $directives[] =
'object-src ' . implode(
' ', $objectSrc );
291 $directives[] =
'report-uri ' . $reportUri;
294 $this->hookRunner->onContentSecurityPolicyDirectives( $directives, $policyConfig, $mode );
296 return implode(
'; ', $directives );
308 'action' =>
'cspreport',
311 if ( $mode === self::REPORT_ONLY_MODE ) {
312 $apiArguments[
'reportonly'] =
'1';
338 if ( preg_match(
'/^[a-z][a-z0-9+.-]*:$/i', $url ) ) {
343 if ( !$bits && strpos( $url,
'/' ) ===
false ) {
349 if ( $bits && isset( $bits[
'host'] )
350 && $bits[
'host'] !== $this->mwConfig->get(
'ServerName' )
352 $result = $bits[
'host'];
353 if ( $bits[
'scheme'] !==
'' ) {
354 $result = $bits[
'scheme'] . $bits[
'delimiter'] . $result;
356 if ( isset( $bits[
'port'] ) ) {
357 $result .=
':' . $bits[
'port'];
370 $additionalUrls = [];
372 $pathVars = [
'LoadScript',
'ExtensionAssetsPath',
'ResourceBasePath' ];
374 foreach ( $pathVars as
$path ) {
375 $url = $this->mwConfig->get(
$path );
377 if ( $preparedUrl ) {
378 $additionalUrls[] = $preparedUrl;
381 $RLSources = $this->mwConfig->get(
'ResourceLoaderSources' );
382 foreach ( $RLSources as $wiki => $sources ) {
383 foreach ( $sources as $id => $value ) {
386 $additionalUrls[] = $url;
391 return array_unique( $additionalUrls );
406 $additionalSelfUrls = [];
413 $callback =
function ( $repo, &$urls ) {
414 $urls[] = $repo->getZoneUrl(
'public' );
415 $urls[] = $repo->getZoneUrl(
'transcoded' );
416 $urls[] = $repo->getZoneUrl(
'thumb' );
417 $urls[] = $repo->getDescriptionStylesheetUrl();
419 $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
420 $localRepo = $repoGroup->getRepo(
'local' );
421 $callback( $localRepo, $pathUrls );
422 $repoGroup->forEachForeignRepo( $callback, [ &$pathUrls ] );
425 $pathGlobals = [
'LoadScript',
'ExtensionAssetsPath',
'StylePath',
'ResourceBasePath' ];
426 foreach ( $pathGlobals as
$path ) {
427 $pathUrls[] = $this->mwConfig->get(
$path );
429 foreach ( $pathUrls as
$path ) {
431 if ( $preparedUrl !==
false ) {
432 $additionalSelfUrls[] = $preparedUrl;
435 $RLSources = $this->mwConfig->get(
'ResourceLoaderSources' );
437 foreach ( $RLSources as $wiki => $sources ) {
438 foreach ( $sources as $id => $value ) {
441 $additionalSelfUrls[] = $url;
446 return array_unique( $additionalSelfUrls );
462 $additionalUrls = [];
463 $CORSSources = $this->mwConfig->get(
'CrossSiteAJAXdomains' );
464 foreach ( $CORSSources as
$source ) {
465 if ( strpos(
$source,
'?' ) !==
false ) {
471 $additionalUrls[] = $url;
474 return $additionalUrls;
503 return (
bool)preg_match(
'!Firefox/4[0-2]\.!', $ua );
514 $config->
get(
'CSPHeader' ),
515 $config->
get(
'CSPReportOnlyHeader' )
527 foreach ( $configs as $headerConfig ) {
529 $headerConfig ===
true ||
530 ( is_array( $headerConfig ) &&
531 !isset( $headerConfig[
'useNonces'] ) ) ||
532 ( is_array( $headerConfig ) &&
533 isset( $headerConfig[
'useNonces'] ) &&
534 $headerConfig[
'useNonces'] )
549 if ( !self::isNonceRequired( $this->mwConfig ) ) {
552 if ( $this->nonce ===
null ) {
553 $rand = random_bytes( 15 );
554 $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)
Add an additional script src.
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)
Get the name of the HTTP header to use.
escapeUrlForCSP( $url)
CSP spec says ',' and ';' are not allowed to appear in urls.
addStyleSrc( $source)
Add an additional CSS src.
string $nonce
The nonce to use for inline scripts (from OutputPage)
getReportUri( $mode)
Get the default report uri.
addDefaultSrc( $source)
Add an additional default src.
getCORSSources()
include domains that are allowed to send us CORS requests.
static isNonceRequiredArray(array $configs)
Does a specific config require a nonce.
getAdditionalSelfUrlsScript()
Get additional script sources.
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.".