Block webpages from navigating to view-source URLs.

This CL blocks navigations to view-source URLs initiated by a page via window.location, window.open, A tag, etc. It still allows user initiated navigations such as directly entering the URL, clicking "view page source" or "open in new tab" in the context menu.

BUG=247151,606619

Review-Url: https://codereview.chromium.org/1917073002
Cr-Commit-Position: refs/heads/master@{#397505}
diff --git a/content/browser/browser_side_navigation_browsertest.cc b/content/browser/browser_side_navigation_browsertest.cc
index 96a2cb9..eaaff053 100644
--- a/content/browser/browser_side_navigation_browsertest.cc
+++ b/content/browser/browser_side_navigation_browsertest.cc
@@ -11,6 +11,7 @@
 #include "content/common/site_isolation_policy.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/content_switches.h"
+#include "content/public/common/url_constants.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/content_browser_test.h"
 #include "content/public/test/content_browser_test_utils.h"
@@ -228,4 +229,48 @@
                   ->GetHasPostData());
 }
 
+// Ensure that browser side navigation can load browser initiated navigations
+// to view-source URLs.
+IN_PROC_BROWSER_TEST_F(BrowserSideNavigationBrowserTest,
+                       ViewSourceNavigation_BrowserInitiated) {
+  TestNavigationObserver observer(shell()->web_contents());
+  GURL url(embedded_test_server()->GetURL("/title1.html"));
+  GURL view_source_url(content::kViewSourceScheme + std::string(":") +
+                       url.spec());
+  NavigateToURL(shell(), view_source_url);
+  EXPECT_EQ(url, observer.last_navigation_url());
+  EXPECT_TRUE(observer.last_navigation_succeeded());
+}
+
+// Ensure that browser side navigation blocks content initiated navigations to
+// view-source URLs.
+IN_PROC_BROWSER_TEST_F(BrowserSideNavigationBrowserTest,
+                       ViewSourceNavigation_RendererInitiated) {
+  TestNavigationObserver observer(shell()->web_contents());
+  GURL kUrl(embedded_test_server()->GetURL("/simple_links.html"));
+  NavigateToURL(shell(), kUrl);
+  EXPECT_EQ(kUrl, observer.last_navigation_url());
+  EXPECT_TRUE(observer.last_navigation_succeeded());
+
+  std::unique_ptr<ConsoleObserverDelegate> console_delegate(
+      new ConsoleObserverDelegate(
+          shell()->web_contents(),
+          "Not allowed to load local resource: view-source:about:blank"));
+  shell()->web_contents()->SetDelegate(console_delegate.get());
+
+  bool success = false;
+  EXPECT_TRUE(ExecuteScriptAndExtractBool(
+      shell()->web_contents(),
+      "window.domAutomationController.send(clickViewSourceLink());", &success));
+  EXPECT_TRUE(success);
+  console_delegate->Wait();
+  // Original page shouldn't navigate away.
+  EXPECT_EQ(kUrl, shell()->web_contents()->GetURL());
+  EXPECT_FALSE(shell()
+                   ->web_contents()
+                   ->GetController()
+                   .GetLastCommittedEntry()
+                   ->IsViewSourceMode());
+}
+
 }  // namespace content
diff --git a/content/browser/child_process_security_policy_impl.cc b/content/browser/child_process_security_policy_impl.cc
index bd84239..d8e601c 100644
--- a/content/browser/child_process_security_policy_impl.cc
+++ b/content/browser/child_process_security_policy_impl.cc
@@ -393,16 +393,6 @@
     return;  // The scheme has already been whitelisted for every child process.
 
   if (IsPseudoScheme(url.scheme())) {
-    // The view-source scheme is a special case of a pseudo-URL that eventually
-    // results in requesting its embedded URL.
-    if (url.SchemeIs(kViewSourceScheme)) {
-      // URLs with the view-source scheme typically look like:
-      //   view-source:http://www.google.com/a
-      // In order to request these URLs, the child_id needs to be able to
-      // request the embedded URL.
-      GrantRequestURL(child_id, GURL(url.GetContent()));
-    }
-
     return;  // Can't grant the capability to request pseudo schemes.
   }
 
@@ -585,25 +575,13 @@
     return false;  // Can't request invalid URLs.
 
   if (IsPseudoScheme(url.scheme())) {
-    // There are a number of special cases for pseudo schemes.
-
-    if (url.SchemeIs(kViewSourceScheme)) {
-      // A view-source URL is allowed if the child process is permitted to
-      // request the embedded URL. Careful to avoid pointless recursion.
-      GURL child_url(url.GetContent());
-      if (child_url.SchemeIs(kViewSourceScheme) &&
-          url.SchemeIs(kViewSourceScheme))
-          return false;
-
-      return CanRequestURL(child_id, child_url);
-    }
-
+    // Every child process can request <about:blank>.
     if (base::LowerCaseEqualsASCII(url.spec(), url::kAboutBlankURL))
-      return true;  // Every child process can request <about:blank>.
-
-    // URLs like <about:version> and <about:crash> shouldn't be requestable by
-    // any child process.  Also, this case covers <javascript:...>, which should
-    // be handled internally by the process and not kicked up to the browser.
+      return true;
+    // URLs like <about:version>, <about:crash>, <view-source:...> shouldn't be
+    // requestable by any child process.  Also, this case covers
+    // <javascript:...>, which should be handled internally by the process and
+    // not kicked up to the browser.
     return false;
   }
 
diff --git a/content/browser/child_process_security_policy_unittest.cc b/content/browser/child_process_security_policy_unittest.cc
index 455da44..24aa45bc2 100644
--- a/content/browser/child_process_security_policy_unittest.cc
+++ b/content/browser/child_process_security_policy_unittest.cc
@@ -169,21 +169,19 @@
   EXPECT_TRUE(p->CanCommitURL(
       kRendererID, GURL("filesystem:http://localhost/temporary/a.gif")));
 
-  // Safe to request but not commit.
-  EXPECT_TRUE(p->CanRequestURL(kRendererID,
-                               GURL("view-source:http://www.google.com/")));
-  EXPECT_FALSE(p->CanCommitURL(kRendererID,
-                               GURL("view-source:http://www.google.com/")));
-
   // Dangerous to request or commit.
   EXPECT_FALSE(p->CanRequestURL(kRendererID,
                                 GURL("file:///etc/passwd")));
   EXPECT_FALSE(p->CanRequestURL(kRendererID,
                                 GURL("chrome://foo/bar")));
+  EXPECT_FALSE(p->CanRequestURL(kRendererID,
+                                GURL("view-source:http://www.google.com/")));
   EXPECT_FALSE(p->CanCommitURL(kRendererID,
                                 GURL("file:///etc/passwd")));
   EXPECT_FALSE(p->CanCommitURL(kRendererID,
                                 GURL("chrome://foo/bar")));
+  EXPECT_FALSE(
+      p->CanCommitURL(kRendererID, GURL("view-source:http://www.google.com/")));
 
   p->Remove(kRendererID);
 }
@@ -300,9 +298,9 @@
 
   p->Add(kRendererID);
 
-  // View source is determined by the embedded scheme.
-  EXPECT_TRUE(p->CanRequestURL(kRendererID,
-                               GURL("view-source:http://www.google.com/")));
+  // Child processes cannot request view source URLs.
+  EXPECT_FALSE(p->CanRequestURL(kRendererID,
+                                GURL("view-source:http://www.google.com/")));
   EXPECT_FALSE(p->CanRequestURL(kRendererID,
                                 GURL("view-source:file:///etc/passwd")));
   EXPECT_FALSE(p->CanRequestURL(kRendererID, GURL("file:///etc/passwd")));
@@ -319,16 +317,13 @@
   EXPECT_FALSE(p->CanCommitURL(
       kRendererID, GURL("view-source:view-source:http://www.google.com/")));
 
-
   p->GrantRequestURL(kRendererID, GURL("view-source:file:///etc/passwd"));
-  // View source needs to be able to request the embedded scheme.
-  EXPECT_TRUE(p->CanRequestURL(kRendererID, GURL("file:///etc/passwd")));
-  EXPECT_TRUE(p->CanCommitURL(kRendererID, GURL("file:///etc/passwd")));
-  EXPECT_TRUE(p->CanRequestURL(kRendererID,
-                               GURL("view-source:file:///etc/passwd")));
+  EXPECT_FALSE(p->CanRequestURL(kRendererID, GURL("file:///etc/passwd")));
+  EXPECT_FALSE(p->CanCommitURL(kRendererID, GURL("file:///etc/passwd")));
+  EXPECT_FALSE(
+      p->CanRequestURL(kRendererID, GURL("view-source:file:///etc/passwd")));
   EXPECT_FALSE(p->CanCommitURL(kRendererID,
                                GURL("view-source:file:///etc/passwd")));
-
   p->Remove(kRendererID);
 }
 
diff --git a/content/browser/site_per_process_browsertest.cc b/content/browser/site_per_process_browsertest.cc
index d8f517e..b4f99d7 100644
--- a/content/browser/site_per_process_browsertest.cc
+++ b/content/browser/site_per_process_browsertest.cc
@@ -463,58 +463,6 @@
   focus_observer.Wait();
 }
 
-// A WebContentsDelegate that catches messages sent to the console.
-class ConsoleObserverDelegate : public WebContentsDelegate {
- public:
-  ConsoleObserverDelegate(WebContents* web_contents, const std::string& filter)
-      : web_contents_(web_contents),
-        filter_(filter),
-        message_(""),
-        message_loop_runner_(new MessageLoopRunner) {}
-
-  ~ConsoleObserverDelegate() override {}
-
-  bool AddMessageToConsole(WebContents* source,
-                           int32_t level,
-                           const base::string16& message,
-                           int32_t line_no,
-                           const base::string16& source_id) override;
-
-  std::string message() { return message_; }
-
-  void Wait();
-
- private:
-  WebContents* web_contents_;
-  std::string filter_;
-  std::string message_;
-
-  // The MessageLoopRunner used to spin the message loop.
-  scoped_refptr<MessageLoopRunner> message_loop_runner_;
-
-  DISALLOW_COPY_AND_ASSIGN(ConsoleObserverDelegate);
-};
-
-void ConsoleObserverDelegate::Wait() {
-  message_loop_runner_->Run();
-}
-
-bool ConsoleObserverDelegate::AddMessageToConsole(
-    WebContents* source,
-    int32_t level,
-    const base::string16& message,
-    int32_t line_no,
-    const base::string16& source_id) {
-  DCHECK(source == web_contents_);
-
-  std::string ascii_message = base::UTF16ToASCII(message);
-  if (base::MatchPattern(ascii_message, filter_)) {
-    message_ = ascii_message;
-    message_loop_runner_->Quit();
-  }
-  return false;
-}
-
 // A BrowserMessageFilter that drops SwapOut ACK messages.
 class SwapoutACKMessageFilter : public BrowserMessageFilter {
  public:
diff --git a/content/browser/web_contents/web_contents_impl_browsertest.cc b/content/browser/web_contents/web_contents_impl_browsertest.cc
index b6593d3f..39e816a9 100644
--- a/content/browser/web_contents/web_contents_impl_browsertest.cc
+++ b/content/browser/web_contents/web_contents_impl_browsertest.cc
@@ -770,6 +770,84 @@
   observer.WaitForPageScaleUpdate();
 }
 
+// Test that a direct navigation to a view-source URL works.
+IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTest, ViewSourceDirectNavigation) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  const GURL kUrl(embedded_test_server()->GetURL("/simple_page.html"));
+  const GURL kViewSourceURL(kViewSourceScheme + std::string(":") + kUrl.spec());
+  NavigateToURL(shell(), kViewSourceURL);
+  // Displayed view-source URLs don't include the scheme of the effective URL if
+  // the effective URL is HTTP. (e.g. view-source:example.com is displayed
+  // instead of view-source:http://example.com).
+  EXPECT_EQ(base::ASCIIToUTF16(std::string("view-source:") + kUrl.host() + ":" +
+                               kUrl.port() + kUrl.path()),
+            shell()->web_contents()->GetTitle());
+  EXPECT_TRUE(shell()
+                  ->web_contents()
+                  ->GetController()
+                  .GetLastCommittedEntry()
+                  ->IsViewSourceMode());
+}
+
+// Test that window.open to a view-source URL is blocked.
+IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTest,
+                       ViewSourceWindowOpen_ShouldBeBlocked) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  const GURL kUrl(embedded_test_server()->GetURL("/simple_page.html"));
+  const GURL kViewSourceURL(kViewSourceScheme + std::string(":") + kUrl.spec());
+  NavigateToURL(shell(), kUrl);
+
+  ShellAddedObserver new_shell_observer;
+  EXPECT_TRUE(ExecuteScript(shell()->web_contents(),
+                            "window.open('" + kViewSourceURL.spec() + "');"));
+  Shell* new_shell = new_shell_observer.GetShell();
+  WaitForLoadStop(new_shell->web_contents());
+  EXPECT_EQ("", new_shell->web_contents()->GetURL().spec());
+  // No navigation should commit.
+  EXPECT_FALSE(
+      new_shell->web_contents()->GetController().GetLastCommittedEntry());
+}
+
+// Test that a content initiated navigation to a view-source URL is blocked.
+IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTest,
+                       ViewSourceRedirect_ShouldBeBlocked) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  const GURL kUrl(embedded_test_server()->GetURL("/simple_page.html"));
+  const GURL kViewSourceURL(kViewSourceScheme + std::string(":") + kUrl.spec());
+  NavigateToURL(shell(), kUrl);
+
+  std::unique_ptr<ConsoleObserverDelegate> console_delegate(
+      new ConsoleObserverDelegate(
+          shell()->web_contents(),
+          "Not allowed to load local resource: view-source:*"));
+  shell()->web_contents()->SetDelegate(console_delegate.get());
+
+  EXPECT_TRUE(
+      ExecuteScript(shell()->web_contents(),
+                    "window.location = '" + kViewSourceURL.spec() + "';"));
+  console_delegate->Wait();
+  // Original page shouldn't navigate away.
+  EXPECT_EQ(kUrl, shell()->web_contents()->GetURL());
+  EXPECT_FALSE(shell()
+                   ->web_contents()
+                   ->GetController()
+                   .GetLastCommittedEntry()
+                   ->IsViewSourceMode());
+}
+
+// Test that view source mode for a webui page can be opened.
+IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTest, ViewSourceWebUI) {
+  const char kUrl[] = "view-source:chrome://chrome/settings";
+  const GURL kGURL(kUrl);
+  NavigateToURL(shell(), kGURL);
+  EXPECT_EQ(base::ASCIIToUTF16(kUrl), shell()->web_contents()->GetTitle());
+  EXPECT_TRUE(shell()
+                  ->web_contents()
+                  ->GetController()
+                  .GetLastCommittedEntry()
+                  ->IsViewSourceMode());
+}
+
 IN_PROC_BROWSER_TEST_F(WebContentsImplBrowserTest, NewNamedWindow) {
   ASSERT_TRUE(embedded_test_server()->Start());
 
diff --git a/content/browser/web_contents/web_contents_impl_unittest.cc b/content/browser/web_contents/web_contents_impl_unittest.cc
index ede10cf..c0bee90 100644
--- a/content/browser/web_contents/web_contents_impl_unittest.cc
+++ b/content/browser/web_contents/web_contents_impl_unittest.cc
@@ -405,29 +405,36 @@
   EXPECT_EQ(title, contents()->GetTitle());
 }
 
-// Test view source mode for a webui page.
-TEST_F(WebContentsImplTest, NTPViewSource) {
+// Browser initiated navigations to view-source URLs of WebUI pages should work.
+TEST_F(WebContentsImplTest, DirectNavigationToViewSourceWebUI) {
   NavigationControllerImpl& cont =
       static_cast<NavigationControllerImpl&>(controller());
-  const char kUrl[] = "view-source:chrome://blah";
-  const GURL kGURL(kUrl);
+  const GURL kGURL("view-source:chrome://blah");
+  // NavigationControllerImpl rewrites view-source URLs, simulating that here.
+  const GURL kRewrittenURL("chrome://blah");
 
   process()->sink().ClearMessages();
 
-  cont.LoadURL(
-      kGURL, Referrer(), ui::PAGE_TRANSITION_TYPED, std::string());
+  // Use LoadURLWithParams instead of LoadURL, because the former properly
+  // rewrites view-source:chrome://blah URLs to chrome://blah.
+  NavigationController::LoadURLParams load_params(kGURL);
+  load_params.transition_type = ui::PAGE_TRANSITION_TYPED;
+  load_params.extra_headers = "content-type: text/plain";
+  load_params.load_type = NavigationController::LOAD_TYPE_DEFAULT;
+  load_params.is_renderer_initiated = false;
+  controller().LoadURLWithParams(load_params);
+
   int entry_id = cont.GetPendingEntry()->GetUniqueID();
   // Did we get the expected message?
   EXPECT_TRUE(process()->sink().GetFirstMessageMatching(
       FrameMsg_EnableViewSourceMode::ID));
 
   FrameHostMsg_DidCommitProvisionalLoad_Params params;
-  InitNavigateParams(&params, 0, entry_id, true, kGURL,
+  InitNavigateParams(&params, 0, entry_id, true, kRewrittenURL,
                      ui::PAGE_TRANSITION_TYPED);
   contents()->GetMainFrame()->PrepareForCommit();
   contents()->GetMainFrame()->SendNavigateWithParams(&params);
-  // Also check title and url.
-  EXPECT_EQ(base::ASCIIToUTF16(kUrl), contents()->GetTitle());
+  EXPECT_EQ(base::ASCIIToUTF16("chrome://blah"), contents()->GetTitle());
 }
 
 // Test to ensure UpdateMaxPageID is working properly.
diff --git a/content/public/test/content_browser_test_utils.cc b/content/public/test/content_browser_test_utils.cc
index daa8e09..6be71c42 100644
--- a/content/public/test/content_browser_test_utils.cc
+++ b/content/public/test/content_browser_test_utils.cc
@@ -8,6 +8,8 @@
 #include "base/files/file_path.h"
 #include "base/path_service.h"
 #include "base/run_loop.h"
+#include "base/strings/pattern.h"
+#include "base/strings/utf_string_conversions.h"
 #include "content/public/browser/navigation_controller.h"
 #include "content/public/browser/notification_source.h"
 #include "content/public/browser/web_contents.h"
@@ -127,5 +129,32 @@
     runner_->QuitClosure().Run();
 }
 
+ConsoleObserverDelegate::ConsoleObserverDelegate(WebContents* web_contents,
+                                                 const std::string& filter)
+    : web_contents_(web_contents),
+      filter_(filter),
+      message_loop_runner_(new MessageLoopRunner) {}
+
+ConsoleObserverDelegate::~ConsoleObserverDelegate() {}
+
+void ConsoleObserverDelegate::Wait() {
+  message_loop_runner_->Run();
+}
+
+bool ConsoleObserverDelegate::AddMessageToConsole(
+    WebContents* source,
+    int32_t level,
+    const base::string16& message,
+    int32_t line_no,
+    const base::string16& source_id) {
+  DCHECK(source == web_contents_);
+
+  std::string ascii_message = base::UTF16ToASCII(message);
+  if (base::MatchPattern(ascii_message, filter_)) {
+    message_ = ascii_message;
+    message_loop_runner_->Quit();
+  }
+  return false;
+}
 
 }  // namespace content
diff --git a/content/public/test/content_browser_test_utils.h b/content/public/test/content_browser_test_utils.h
index 47b2c07..2258569 100644
--- a/content/public/test/content_browser_test_utils.h
+++ b/content/public/test/content_browser_test_utils.h
@@ -8,6 +8,7 @@
 #include "base/callback.h"
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
+#include "content/public/browser/web_contents_delegate.h"
 #include "content/public/common/page_type.h"
 #include "ui/gfx/native_widget_types.h"
 #include "url/gurl.h"
@@ -29,6 +30,7 @@
 
 class MessageLoopRunner;
 class Shell;
+class WebContents;
 
 // Generate the file path for testing a particular test.
 // The file for the tests is all located in
@@ -98,6 +100,37 @@
   DISALLOW_COPY_AND_ASSIGN(ShellAddedObserver);
 };
 
+// A WebContentsDelegate that catches messages sent to the console.
+class ConsoleObserverDelegate : public WebContentsDelegate {
+ public:
+  ConsoleObserverDelegate(WebContents* web_contents, const std::string& filter);
+  ~ConsoleObserverDelegate() override;
+
+  // WebContentsDelegate method:
+  bool AddMessageToConsole(WebContents* source,
+                           int32_t level,
+                           const base::string16& message,
+                           int32_t line_no,
+                           const base::string16& source_id) override;
+
+  // Returns the most recent message sent to the console.
+  std::string message() { return message_; }
+
+  // Waits for the next message captured by the filter to be sent to the
+  // console.
+  void Wait();
+
+ private:
+  WebContents* web_contents_;
+  std::string filter_;
+  std::string message_;
+
+  // The MessageLoopRunner used to spin the message loop.
+  scoped_refptr<MessageLoopRunner> message_loop_runner_;
+
+  DISALLOW_COPY_AND_ASSIGN(ConsoleObserverDelegate);
+};
+
 #if defined OS_MACOSX
 void SetWindowBounds(gfx::NativeWindow window, const gfx::Rect& bounds);
 #endif
diff --git a/content/renderer/render_thread_impl.cc b/content/renderer/render_thread_impl.cc
index d17d359..b744190 100644
--- a/content/renderer/render_thread_impl.cc
+++ b/content/renderer/render_thread_impl.cc
@@ -1268,6 +1268,10 @@
   // chrome-devtools:
   WebString devtools_scheme(base::ASCIIToUTF16(kChromeDevToolsScheme));
   WebSecurityPolicy::registerURLSchemeAsDisplayIsolated(devtools_scheme);
+
+  // view-source:
+  WebString view_source_scheme(base::ASCIIToUTF16(kViewSourceScheme));
+  WebSecurityPolicy::registerURLSchemeAsDisplayIsolated(view_source_scheme);
 }
 
 void RenderThreadImpl::NotifyTimezoneChange() {
diff --git a/content/test/data/simple_links.html b/content/test/data/simple_links.html
index e5206796..43f7405c 100644
--- a/content/test/data/simple_links.html
+++ b/content/test/data/simple_links.html
@@ -24,10 +24,15 @@
   function clickCrossSiteLink() {
     return simulateClick(document.getElementById("cross_site_link"));
   }
+
+  function clickViewSourceLink() {
+    return simulateClick(document.getElementById("view_source_link"));
+  }
  </script>
  </head>
 
 <a href="title2.html" id="same_site_link">same-site</a><br>
 <a href="http://foo.com/title2.html" id="cross_site_link">cross-site</a><br>
+<a href="view-source:about:blank" id="view_source_link">view-source:</a><br>
 
 </html>