Never bypass CSP in WebUI pages

BUG=668645
TEST=browser_test --gtest_filter=ExtensionCSPBypassTest.LoadWebAccessibleScript

Change-Id: I03a96a742b06ca6ec81fad95aae17cbdb11e1cce
Reviewed-on: https://chromium-review.googlesource.com/567499
Commit-Queue: Rob Wu <rob@robwu.nl>
Reviewed-by: Devlin <rdevlin.cronin@chromium.org>
Reviewed-by: Mike West <mkwst@chromium.org>
Cr-Commit-Position: refs/heads/master@{#528343}
diff --git a/chrome/browser/extensions/extension_csp_bypass_browsertest.cc b/chrome/browser/extensions/extension_csp_bypass_browsertest.cc
new file mode 100644
index 0000000..bd852ab
--- /dev/null
+++ b/chrome/browser/extensions/extension_csp_bypass_browsertest.cc
@@ -0,0 +1,119 @@
+// Copyright (c) 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/strings/stringprintf.h"
+#include "chrome/browser/extensions/extension_browsertest.h"
+#include "chrome/browser/extensions/test_extension_dir.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_test_utils.h"
+#include "extensions/common/value_builder.h"
+
+namespace extensions {
+
+namespace {
+
+class ExtensionCSPBypassTest : public ExtensionBrowserTest {
+ public:
+  ExtensionCSPBypassTest() {}
+
+  void SetUpOnMainThread() override {
+    ExtensionBrowserTest::SetUpOnMainThread();
+    ASSERT_TRUE(embedded_test_server()->Start());
+  }
+
+ protected:
+  const Extension* AddExtension(bool is_component, bool all_urls_permission) {
+    auto dir = std::make_unique<TestExtensionDir>();
+
+    std::string unique_name = base::StringPrintf(
+        "component=%d, all_urls=%d", is_component, all_urls_permission);
+    DictionaryBuilder manifest;
+    manifest.Set("name", unique_name)
+        .Set("version", "1")
+        .Set("manifest_version", 2)
+        .Set("web_accessible_resources", ListBuilder().Append("*").Build());
+
+    if (all_urls_permission) {
+      manifest.Set("permissions", ListBuilder().Append("<all_urls>").Build());
+    }
+    if (is_component) {
+      // LoadExtensionAsComponent requires the manifest to contain a key.
+      std::string key;
+      EXPECT_TRUE(Extension::ProducePEM(unique_name, &key));
+      manifest.Set("key", key);
+    }
+
+    dir->WriteFile(FILE_PATH_LITERAL("script.js"), "");
+    dir->WriteManifest(manifest.ToJSON());
+
+    const Extension* extension = nullptr;
+    if (is_component) {
+      extension = LoadExtensionAsComponent(dir->UnpackedPath());
+    } else {
+      extension = LoadExtension(dir->UnpackedPath());
+    }
+    CHECK(extension);
+    temp_dirs_.push_back(std::move(dir));
+    return extension;
+  }
+
+  bool CanLoadScript(const Extension* extension) {
+    content::RenderFrameHost* rfh =
+        browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();
+    std::string code = base::StringPrintf(
+        R"(
+        var s = document.createElement('script');
+        s.src = '%s';
+        s.onload = function() {
+          // Not blocked by CSP.
+          window.domAutomationController.send(true);
+        };
+        s.onerror = function() {
+          // Blocked by CSP.
+          window.domAutomationController.send(false);
+        };
+        document.body.appendChild(s);)",
+        extension->GetResourceURL("script.js").spec().c_str());
+    bool script_loaded = false;
+    EXPECT_TRUE(ExecuteScriptAndExtractBool(rfh, code, &script_loaded));
+    return script_loaded;
+  }
+
+ private:
+  std::vector<std::unique_ptr<TestExtensionDir>> temp_dirs_;
+
+  DISALLOW_COPY_AND_ASSIGN(ExtensionCSPBypassTest);
+};
+
+}  // namespace
+
+IN_PROC_BROWSER_TEST_F(ExtensionCSPBypassTest, LoadWebAccessibleScript) {
+  const Extension* component_ext_with_permission = AddExtension(true, true);
+  const Extension* component_ext_without_permission = AddExtension(true, false);
+  const Extension* ext_with_permission = AddExtension(false, true);
+  const Extension* ext_without_permission = AddExtension(false, false);
+
+  // chrome-extension:-URLs can always bypass CSP in normal pages.
+  GURL non_webui_url(embedded_test_server()->GetURL("/empty.html"));
+  ui_test_utils::NavigateToURL(browser(), non_webui_url);
+
+  EXPECT_TRUE(CanLoadScript(component_ext_with_permission));
+  EXPECT_TRUE(CanLoadScript(component_ext_without_permission));
+  EXPECT_TRUE(CanLoadScript(ext_with_permission));
+  EXPECT_TRUE(CanLoadScript(ext_without_permission));
+
+  // chrome-extension:-URLs can never bypass CSP in WebUI.
+  ui_test_utils::NavigateToURL(browser(), GURL(chrome::kChromeUIVersionURL));
+
+  EXPECT_FALSE(CanLoadScript(component_ext_with_permission));
+  EXPECT_FALSE(CanLoadScript(component_ext_without_permission));
+  EXPECT_FALSE(CanLoadScript(ext_with_permission));
+  EXPECT_FALSE(CanLoadScript(ext_without_permission));
+}
+
+}  // namespace extensions
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 1802bfe..44d59b6 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -1216,6 +1216,7 @@
         "../browser/extensions/extension_browsertest.cc",
         "../browser/extensions/extension_browsertest.h",
         "../browser/extensions/extension_context_menu_browsertest.cc",
+        "../browser/extensions/extension_csp_bypass_browsertest.cc",
         "../browser/extensions/extension_disabled_ui_browsertest.cc",
         "../browser/extensions/extension_dom_clipboard_apitest.cc",
         "../browser/extensions/extension_fileapi_apitest.cc",
diff --git a/third_party/WebKit/Source/core/frame/csp/ContentSecurityPolicy.cpp b/third_party/WebKit/Source/core/frame/csp/ContentSecurityPolicy.cpp
index bb3994c..fbb0abb 100644
--- a/third_party/WebKit/Source/core/frame/csp/ContentSecurityPolicy.cpp
+++ b/third_party/WebKit/Source/core/frame/csp/ContentSecurityPolicy.cpp
@@ -684,9 +684,13 @@
     // regardless of parser state. Once we have more data via the
     // 'ScriptWithCSPBypassingScheme*' metrics, make a decision about what
     // behavior to ship. https://crbug.com/653521
-    if (parser_disposition == kNotParserInserted ||
-        !RuntimeEnabledFeatures::
-            ExperimentalContentSecurityPolicyFeaturesEnabled()) {
+    if ((parser_disposition == kNotParserInserted ||
+         !RuntimeEnabledFeatures::
+             ExperimentalContentSecurityPolicyFeaturesEnabled()) &&
+        // The schemes where javascript:-URLs are blocked are usually privileged
+        // pages, so do not allow the CSP to be bypassed either.
+        !SchemeRegistry::ShouldTreatURLSchemeAsNotAllowingJavascriptURLs(
+            execution_context_->GetSecurityOrigin()->Protocol())) {
       return true;
     }
   }
diff --git a/third_party/WebKit/Source/core/frame/csp/ContentSecurityPolicyTest.cpp b/third_party/WebKit/Source/core/frame/csp/ContentSecurityPolicyTest.cpp
index 72f40a320..e0c3926 100644
--- a/third_party/WebKit/Source/core/frame/csp/ContentSecurityPolicyTest.cpp
+++ b/third_party/WebKit/Source/core/frame/csp/ContentSecurityPolicyTest.cpp
@@ -1176,6 +1176,52 @@
       "https");
 }
 
+TEST_F(ContentSecurityPolicyTest, CSPBypassDisabledWhenSchemeIsPrivileged) {
+  const KURL base;
+  execution_context = CreateExecutionContext();
+  execution_context->SetSecurityOrigin(secure_origin);
+  execution_context->SetURL(BlankURL());
+  csp->BindToExecutionContext(execution_context.Get());
+  csp->DidReceiveHeader("script-src http://example.com",
+                        kContentSecurityPolicyHeaderTypeEnforce,
+                        kContentSecurityPolicyHeaderSourceHTTP);
+
+  const KURL allowed_url("http://example.com/script.js");
+  const KURL http_url("http://not-example.com/script.js");
+  const KURL blob_url(base, "blob:http://not-example.com/uuid");
+  const KURL filesystem_url(base, "filesystem:http://not-example.com/file.js");
+
+  // The {Requests,Blob,Filesystem}AllowedWhenBypassingCSP tests have already
+  // ensured that RegisterURLSchemeAsBypassingContentSecurityPolicy works as
+  // expected.
+  //
+  // "http" is registered as bypassing CSP, but the context's scheme ("https")
+  // is marked as a privileged scheme, so the bypass rule should be ignored.
+  SchemeRegistry::RegisterURLSchemeAsBypassingContentSecurityPolicy("http");
+  SchemeRegistry::RegisterURLSchemeAsNotAllowingJavascriptURLs("https");
+
+  EXPECT_TRUE(csp->AllowScriptFromSource(
+      allowed_url, String(), IntegrityMetadataSet(), kNotParserInserted,
+      ResourceRequest::RedirectStatus::kNoRedirect,
+      SecurityViolationReportingPolicy::kSuppressReporting));
+  EXPECT_FALSE(csp->AllowScriptFromSource(
+      http_url, String(), IntegrityMetadataSet(), kNotParserInserted,
+      ResourceRequest::RedirectStatus::kNoRedirect,
+      SecurityViolationReportingPolicy::kSuppressReporting));
+  EXPECT_FALSE(csp->AllowScriptFromSource(
+      blob_url, String(), IntegrityMetadataSet(), kNotParserInserted,
+      ResourceRequest::RedirectStatus::kNoRedirect,
+      SecurityViolationReportingPolicy::kSuppressReporting));
+  EXPECT_FALSE(csp->AllowScriptFromSource(
+      filesystem_url, String(), IntegrityMetadataSet(), kNotParserInserted,
+      ResourceRequest::RedirectStatus::kNoRedirect,
+      SecurityViolationReportingPolicy::kSuppressReporting));
+
+  SchemeRegistry::RemoveURLSchemeRegisteredAsBypassingContentSecurityPolicy(
+      "http");
+  SchemeRegistry::RemoveURLSchemeAsNotAllowingJavascriptURLs("https");
+}
+
 TEST_F(ContentSecurityPolicyTest, IsValidCSPAttrTest) {
   // Empty string is invalid
   EXPECT_FALSE(ContentSecurityPolicy::IsValidCSPAttr(""));
diff --git a/third_party/WebKit/Source/platform/weborigin/SchemeRegistry.cpp b/third_party/WebKit/Source/platform/weborigin/SchemeRegistry.cpp
index e73e1b00..4b18af9 100644
--- a/third_party/WebKit/Source/platform/weborigin/SchemeRegistry.cpp
+++ b/third_party/WebKit/Source/platform/weborigin/SchemeRegistry.cpp
@@ -209,6 +209,12 @@
       scheme);
 }
 
+void SchemeRegistry::RemoveURLSchemeAsNotAllowingJavascriptURLs(
+    const String& scheme) {
+  GetMutableURLSchemesRegistry().not_allowing_javascript_urls_schemes.erase(
+      scheme);
+}
+
 bool SchemeRegistry::ShouldTreatURLSchemeAsNotAllowingJavascriptURLs(
     const String& scheme) {
   DCHECK_EQ(scheme, scheme.LowerASCII());
diff --git a/third_party/WebKit/Source/platform/weborigin/SchemeRegistry.h b/third_party/WebKit/Source/platform/weborigin/SchemeRegistry.h
index d3d54a4..d77dc0c 100644
--- a/third_party/WebKit/Source/platform/weborigin/SchemeRegistry.h
+++ b/third_party/WebKit/Source/platform/weborigin/SchemeRegistry.h
@@ -83,6 +83,7 @@
   // bookmarklets from running on sensitive pages).
   static void RegisterURLSchemeAsNotAllowingJavascriptURLs(
       const String& scheme);
+  static void RemoveURLSchemeAsNotAllowingJavascriptURLs(const String& scheme);
   static bool ShouldTreatURLSchemeAsNotAllowingJavascriptURLs(
       const String& scheme);