From 146e9c96ea85d309d7b2b851b4b4443e5bc18f91 Mon Sep 17 00:00:00 2001 From: Brian Wolff Date: Mon, 2 Jul 2018 06:19:43 +0000 Subject: [PATCH] resourceloader: Give module eval the ContentSecurityPolicy nonce Previously domEval didn't have CSP nonces, causing it to violate the policy. Also removes the meta tag scheme, as I could not make it compatible with how RL storage works using domEval instead of real eval() and it didn't provide much protection anyways. Bug: T196923 Change-Id: I3cd2d7cc295c39b498d0bf37915d4ba167fdd48c --- includes/ContentSecurityPolicy.php | 79 +++---------------- includes/OutputPage.php | 1 + resources/src/startup/mediawiki.js | 3 + .../includes/ContentSecurityPolicyTest.php | 23 +----- 4 files changed, 19 insertions(+), 87 deletions(-) diff --git a/includes/ContentSecurityPolicy.php b/includes/ContentSecurityPolicy.php index 91117f4e36..62160461c9 100644 --- a/includes/ContentSecurityPolicy.php +++ b/includes/ContentSecurityPolicy.php @@ -27,8 +27,6 @@ class ContentSecurityPolicy { const REPORT_ONLY_MODE = 1; const FULL_MODE = 2; - /** Used for meta tag. Does not include report urls or nonce sources */ - const FULL_MODE_RESTRICTED = 3; /** @var string The nonce to use for inline scripts (from OutputPage) */ private $nonce; @@ -65,20 +63,6 @@ class ContentSecurityPolicy { } } - /** - * Return the meta header to use for after load restricted mode - * - * This should restrict browsers that don't support nonce-sources. - * Idea stolen from - * https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/ - * - * @param array $csp CSP configuration - * @return string Content for meta tag - */ - public function getMetaHeader( $csp ) { - return $this->makeCSPDirectives( $csp, self::FULL_MODE_RESTRICTED ); - } - /** * Send CSP headers based on wiki config * @@ -100,39 +84,13 @@ class ContentSecurityPolicy { $csp->sendCSPHeader( $cspConfig, self::FULL_MODE ); $csp->sendCSPHeader( $cspConfigReportOnly, self::REPORT_ONLY_MODE ); - // Include header which increases security level after initial load. - // This helps mitigate attacks on browsers not supporting CSP2. It also - // helps mitigate attacks due to the shared nonce that non-logged in users - // get due to varnish cache. - // Unclear if this is the best place to insert the meta tag, or if - // it should be in a RL module. I figure its best to do this as early - // as possible. - // FIXME: Needs testing to see if this actually works properly - $metaHeader = $csp->getMetaHeader( $cspConfig ); - if ( $metaHeader ) { - $context->getOutput()->addScript( - ResourceLoader::makeInlineScript( - $csp->makeMetaInsertScript( - $metaHeader - ), - $out->getCSPNonce() - ) - ); - } - } - - /** - * Makes javascript to insert a meta CSP header after page load - * - * @see https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/ - * @param string $metaContents content of meta tag - * @return string JS for including in page - */ - private function makeMetaInsertScript( $metaContents ) { - return "$('\\x3Cmeta http-equiv=\"Content-Security-Policy\"\\x3E')" . - '.attr("content",' . - Xml::encodeJsVar( $metaContents ) . - ').prependTo($("head"))'; + // This used to insert a tag here, per advice at + // https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/ + // The goal was to prevent nonce from working after the page hit onready, + // This would help in old browsers that didn't support nonces, and + // also assist for varnish-cached pages which repeat nonces. + // However, this is incompatible with how resource loader storage works + // via mw.domEval() so it was removed. } /** @@ -140,7 +98,6 @@ class ContentSecurityPolicy { * * @param int $reportOnly Either self::REPORT_ONLY_MODE or self::FULL_MODE * @return string Name of http header - * @throws UnexpectedValueException if you feed it self::FULL_MODE_RESTRICTED. */ private function getHeaderName( $reportOnly ) { if ( $reportOnly === self::REPORT_ONLY_MODE ) { @@ -155,7 +112,7 @@ class ContentSecurityPolicy { * Determine what CSP policies to set for this page * * @param array|bool $config Policy configuration (Either $wgCSPHeader or $wgCSPReportOnlyHeader) - * @param int $mode self::REPORT_ONLY_MODE, self::FULL_MODE or Self::FULL_MODE_RESTRICTED + * @param int $mode self::REPORT_ONLY_MODE, self::FULL_MODE * @return string Policy directives, or empty string for no policy. */ private function makeCSPDirectives( $policyConfig, $mode ) { @@ -182,13 +139,10 @@ class ContentSecurityPolicy { $cssSrc = false; $imgSrc = false; $scriptSrc = [ "'unsafe-eval'", "'self'" ]; - if ( - $mode !== self::FULL_MODE_RESTRICTED && - ( !isset( $policyConfig['useNonces'] ) || $policyConfig['useNonces'] ) - ) { - $nonceSrc = "'nonce-" . $this->nonce . "'"; - $scriptSrc[] = $nonceSrc; + if ( !isset( $policyConfig['useNonces'] ) || $policyConfig['useNonces'] ) { + $scriptSrc[] = "'nonce-" . $this->nonce . "'"; } + $scriptSrc = array_merge( $scriptSrc, $additionalSelfUrlsScript ); if ( isset( $policyConfig['script-src'] ) && is_array( $policyConfig['script-src'] ) @@ -200,7 +154,6 @@ class ContentSecurityPolicy { // Note: default on if unspecified. if ( ( !isset( $policyConfig['unsafeFallback'] ) || $policyConfig['unsafeFallback'] ) - && $mode !== self::FULL_MODE_RESTRICTED ) { // unsafe-inline should be ignored on browsers // that support 'nonce-foo' sources. @@ -247,10 +200,7 @@ class ContentSecurityPolicy { $cssSrc = array_merge( $defaultSrc, [ "'unsafe-inline'" ] ); } - if ( $mode === self::FULL_MODE_RESTRICTED ) { - // report-uri disallowed in tags. - $reportUri = false; - } elseif ( isset( $policyConfig['report-uri'] ) && $policyConfig['report-uri'] !== true ) { + if ( isset( $policyConfig['report-uri'] ) && $policyConfig['report-uri'] !== true ) { if ( $policyConfig['report-uri'] === false ) { $reportUri = false; } else { @@ -311,14 +261,11 @@ class ContentSecurityPolicy { /** * Get the default report uri. * - * @param int $mode self::*_MODE constant. Do not use with self::FULL_MODE_RESTRICTED + * @param int $mode self::*_MODE constant. * @return string The URI to send reports to. * @throws UnexpectedValueException if given invalid mode. */ private function getReportUri( $mode ) { - if ( $mode === self::FULL_MODE_RESTRICTED ) { - throw new UnexpectedValueException( $mode ); - } $apiArguments = [ 'action' => 'cspreport', 'format' => 'json' diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 9173f26b17..3b91331a5c 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -3125,6 +3125,7 @@ class OutputPage extends ContextSource { 'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(), 'wgRelevantArticleId' => $relevantTitle->getArticleID(), 'wgRequestId' => WebRequest::getRequestId(), + 'wgCSPNonce' => $this->getCSPNonce(), ]; if ( $user->isLoggedIn() ) { diff --git a/resources/src/startup/mediawiki.js b/resources/src/startup/mediawiki.js index 8d42c0f024..e2db9ea30c 100644 --- a/resources/src/startup/mediawiki.js +++ b/resources/src/startup/mediawiki.js @@ -1028,6 +1028,9 @@ */ function domEval( code ) { var script = document.createElement( 'script' ); + if ( mw.config.get( 'wgCSPNonce' ) !== false ) { + script.nonce = mw.config.get( 'wgCSPNonce' ); + } script.text = code; document.head.appendChild( script ); script.parentNode.removeChild( script ); diff --git a/tests/phpunit/includes/ContentSecurityPolicyTest.php b/tests/phpunit/includes/ContentSecurityPolicyTest.php index c383be6ff2..250d49d720 100644 --- a/tests/phpunit/includes/ContentSecurityPolicyTest.php +++ b/tests/phpunit/includes/ContentSecurityPolicyTest.php @@ -72,26 +72,21 @@ class ContentSecurityPolicyTest extends MediaWikiTestCase { public function testMakeCSPDirectives( $policy, $expectedFull, - $expectedReport, - $expectedRestricted + $expectedReport ) { $actualFull = $this->csp->makeCSPDirectives( $policy, ContentSecurityPolicy::FULL_MODE ); $actualReport = $this->csp->makeCSPDirectives( $policy, ContentSecurityPolicy::REPORT_ONLY_MODE ); - $actualRestricted = $this->csp->makeCSPDirectives( - $policy, ContentSecurityPolicy::FULL_MODE_RESTRICTED - ); $policyJson = formatJson::encode( $policy ); $this->assertEquals( $expectedFull, $actualFull, "full: " . $policyJson ); $this->assertEquals( $expectedReport, $actualReport, "report: " . $policyJson ); - $this->assertEquals( $expectedRestricted, $actualRestricted, "restricted: " . $policyJson ); } public function providerMakeCSPDirectives() { // @codingStandardsIgnoreStart Generic.Files.LineLength return [ - [ false, '', '', '' ], + [ false, '', '' ], [ [ 'useNonces' => false ], "script-src 'unsafe-eval' 'self' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", @@ -102,85 +97,71 @@ class ContentSecurityPolicyTest extends MediaWikiTestCase { true, "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", ], [ [], "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", ], [ [ 'script-src' => [ 'http://example.com', 'http://something,else.com' ] ], "script-src 'unsafe-eval' 'self' 'nonce-secret' http://example.com http://something%2Celse.com 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' http://example.com http://something%2Celse.com 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self' http://example.com http://something%2Celse.com sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", ], [ [ 'unsafeFallback' => false ], "script-src 'unsafe-eval' 'self' 'nonce-secret' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", ], [ [ 'unsafeFallback' => true ], "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", ], [ [ 'default-src' => false ], "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", ], [ [ 'default-src' => true ], "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'", ], [ [ 'default-src' => [ 'https://foo.com', 'http://bar.com', 'baz.de' ] ], "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org https://foo.com http://bar.com baz.de sister-site.somewhere.com *.wikipedia.org 'unsafe-inline'", ], [ [ 'includeCORS' => false ], "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline'; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline'; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self'; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", ], [ [ 'includeCORS' => false, 'default-src' => true ], "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline'; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline'; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self'; default-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org; style-src 'self' data: blob: https://upload.wikimedia.org https://commons.wikimedia.org 'unsafe-inline'", ], [ [ 'includeCORS' => true ], "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", ], [ [ 'report-uri' => false ], "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", - "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", ], [ [ 'report-uri' => true ], "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&", - "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", ], [ [ 'report-uri' => 'https://example.com/index.php?foo;report=csp' ], "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri https://example.com/index.php?foo%3Breport=csp", "script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri https://example.com/index.php?foo%3Breport=csp", - "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'", ], ]; } -- 2.20.1