resourceloader: Give module eval the ContentSecurityPolicy nonce
authorBrian Wolff <bawolff+wn@gmail.com>
Mon, 2 Jul 2018 06:19:43 +0000 (06:19 +0000)
committerKrinkle <krinklemail@gmail.com>
Tue, 7 Aug 2018 16:54:40 +0000 (16:54 +0000)
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
includes/OutputPage.php
resources/src/startup/mediawiki.js
tests/phpunit/includes/ContentSecurityPolicyTest.php

index 91117f4..6216046 100644 (file)
@@ -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 <meta> 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 <meta> 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 <meta> 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'
index 9173f26..3b91331 100644 (file)
@@ -3125,6 +3125,7 @@ class OutputPage extends ContextSource {
                        'wgRelevantPageName' => $relevantTitle->getPrefixedDBkey(),
                        'wgRelevantArticleId' => $relevantTitle->getArticleID(),
                        'wgRequestId' => WebRequest::getRequestId(),
+                       'wgCSPNonce' => $this->getCSPNonce(),
                ];
 
                if ( $user->isLoggedIn() ) {
index 8d42c0f..e2db9ea 100644 (file)
                         */
                        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 );
index c383be6..250d49d 100644 (file)
@@ -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'",
                        ],
                ];
        }