OOR-CORS: each content:// should be assumed as an opaque origin

On Android Chrome, each content:// should be assumed as an opaque
origin, and should not allow CORS-enabled requests among content://
URLs. Also content:// can not load legacy worker scripts from
content:// URLs as the mode "same-origin" is not permitted too.

TEST=./out/a/bin/run_chrome_public_test_apk -A Feature=CORS

Bug: 1092449
Change-Id: I83d15f4c1e2f2d88e219032952a7da78f470c16a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2247920
Commit-Queue: Takashi Toyoshima <toyoshim@chromium.org>
Reviewed-by: Tommy Nyquist <nyquist@chromium.org>
Reviewed-by: Yutaka Hirano <yhirano@chromium.org>
Reviewed-by: Kinuko Yasuda <kinuko@chromium.org>
Cr-Commit-Position: refs/heads/master@{#779596}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/UrlSchemeTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/UrlSchemeTest.java
index fbf4acb..98a2f6b 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/UrlSchemeTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/UrlSchemeTest.java
@@ -34,6 +34,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URLEncoder;
 
 /** Test suite for different Android URL schemes. */
 @RunWith(ChromeJUnit4ClassRunner.class)
@@ -140,6 +141,85 @@
         });
     }
 
+    private String loadContentUrlToMakeCorsToContent(final String api, final String mode)
+            throws Throwable {
+        final String resource = "content_url_make_cors_to_content.html";
+        final String imageUrl = createContentUrl("google.png");
+
+        mActivityTestRule.loadUrl(createContentUrl(resource) + "?api=" + api + "&mode=" + mode
+                + "&url=" + URLEncoder.encode(imageUrl));
+
+        // Make sure the CORS request fail in the page.
+        CriteriaHelper.pollUiThread(new Criteria() {
+            @Override
+            public boolean isSatisfied() {
+                return !mActivityTestRule.getActivity().getActivityTab().getTitle().equals(
+                        "running");
+            }
+        });
+
+        // Make sure that content provider was asked to provide the content.
+        ensureResourceRequestCountInContentProviderNotLessThan(resource, 1);
+
+        return mActivityTestRule.getActivity().getActivityTab().getTitle();
+    }
+
+    @Test
+    @MediumTest
+    @Feature({"CORS"})
+    public void testContentUrlCanNotMakeXhrRequestToContentUrl() throws Throwable {
+        // The XMLHttpRequest can carry content:// URLs, but CORS requests for content:// are not
+        // permitted.
+        Assert.assertEquals("error", loadContentUrlToMakeCorsToContent("xhr", ""));
+    }
+
+    @Test
+    @MediumTest
+    @Feature({"CORS"})
+    public void testContentUrlCanNotMakeFetchCorsRequestToContentUrl() throws Throwable {
+        // The Fetch API does not support content:// scheme.
+        Assert.assertEquals("error", loadContentUrlToMakeCorsToContent("fetch", "cors"));
+    }
+
+    @Test
+    @MediumTest
+    @Feature({"CORS"})
+    public void testContentUrlCanNotMakeFetchSameOriginRequestToContentUrl() throws Throwable {
+        // The Fetch API does not support content:// scheme.
+        Assert.assertEquals("error", loadContentUrlToMakeCorsToContent("fetch", "same-origin"));
+    }
+
+    @Test
+    @MediumTest
+    @Feature({"CORS"})
+    public void testContentUrlCanNotMakeFetchNoCorsRequestToContentUrl() throws Throwable {
+        // The Fetch API does not support content:// scheme.
+        Assert.assertEquals("error", loadContentUrlToMakeCorsToContent("fetch", "no-cors"));
+    }
+
+    @Test
+    @MediumTest
+    @Feature({"CORS"})
+    public void testContentUrlToLoadWorkerFromContent() throws Throwable {
+        final String resource = "content_url_load_content_worker.html";
+
+        mActivityTestRule.loadUrl(createContentUrl(resource));
+
+        CriteriaHelper.pollUiThread(new Criteria() {
+            @Override
+            public boolean isSatisfied() {
+                return !mActivityTestRule.getActivity().getActivityTab().getTitle().equals(
+                        "running");
+            }
+        });
+
+        // Make sure that content provider was asked to provide the content.
+        ensureResourceRequestCountInContentProviderNotLessThan(resource, 1);
+
+        Assert.assertEquals(
+                "exception", mActivityTestRule.getActivity().getActivityTab().getTitle());
+    }
+
     /**
      * Test that a content URL is *ALLOWED* to access an image provided by a content URL.
      */
diff --git a/chrome/test/data/android/content_url_load_content_worker.html b/chrome/test/data/android/content_url_load_content_worker.html
new file mode 100644
index 0000000..5959ea72
--- /dev/null
+++ b/chrome/test/data/android/content_url_load_content_worker.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<!-- can not be in a sub-directory as TestContentProvider does not support it -->
+<html>
+<head><title>running</title></head>
+<body><script>
+try {
+  const worker = new Worker('./worker.js');
+  worker.onerror = e => document.title = 'error';
+  worker.onmessage = e => document.title = e.data;
+  worker.postMessage([]);
+} catch (e) {
+  document.title = 'exception'
+}
+</script></body>
+</html>
diff --git a/chrome/test/data/android/content_url_make_cors_to_content.html b/chrome/test/data/android/content_url_make_cors_to_content.html
new file mode 100644
index 0000000..c76acb12
--- /dev/null
+++ b/chrome/test/data/android/content_url_make_cors_to_content.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<!-- can not be in a sub-directory as TestContentProvider does not support it -->
+<html>
+<head><title>running</title></head>
+<body><script>
+window.onload = function () {
+  const params = new URL(document.location).searchParams;
+  const api = params.get('api');
+  if (api == "xhr") {
+    const xhr = new XMLHttpRequest();
+    xhr.open('get', params.get('url'), true);
+    xhr.onload = e => document.title = 'load';
+    xhr.onerror = e => document.title = 'error';
+    xhr.onabort = e => document.title = 'abort';
+    xhr.send();
+  } else if (api == "fetch") {
+    fetch(params.get('url'), { 'mode': params.get('mode') }).then(
+        e => document.title = 'load',
+        e => document.title = 'error');
+  } else {
+    document.title = "unknown api"
+  }
+};
+</script></body>
+</html>
diff --git a/chrome/test/data/android/worker.js b/chrome/test/data/android/worker.js
new file mode 100644
index 0000000..44dd00c
--- /dev/null
+++ b/chrome/test/data/android/worker.js
@@ -0,0 +1,9 @@
+// Copyright 2020 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.
+
+// This file is accessed via content:// URL and can not be in a sub-directory
+// as TestContentProvider does not support it.
+onmessage = function(e) {
+  postMessage('load');
+}
diff --git a/content/browser/android/content_url_loader_factory.cc b/content/browser/android/content_url_loader_factory.cc
index e0125f1..0be595d 100644
--- a/content/browser/android/content_url_loader_factory.cc
+++ b/content/browser/android/content_url_loader_factory.cc
@@ -20,6 +20,7 @@
 #include "content/public/browser/content_browser_client.h"
 #include "content/public/browser/file_url_loader.h"
 #include "content/public/common/content_client.h"
+#include "content/public/common/content_switches.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "mojo/public/cpp/system/data_pipe.h"
@@ -28,6 +29,7 @@
 #include "net/base/net_errors.h"
 #include "net/http/http_byte_range.h"
 #include "net/http/http_util.h"
+#include "services/network/public/cpp/cors/cors.h"
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
@@ -126,8 +128,28 @@
       const network::ResourceRequest& request,
       mojo::PendingReceiver<network::mojom::URLLoader> loader,
       mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote) {
+    bool disable_web_security =
+        base::CommandLine::ForCurrentProcess()->HasSwitch(
+            switches::kDisableWebSecurity);
+    network::mojom::FetchResponseType response_type =
+        network::cors::CalculateResponseType(request.mode,
+                                             disable_web_security);
+
+    // Don't allow content:// requests with kSameOrigin or kCors* unless the
+    // web security is turned off.
+    if ((!disable_web_security &&
+         request.mode == network::mojom::RequestMode::kSameOrigin) ||
+        response_type == network::mojom::FetchResponseType::kCors) {
+      mojo::Remote<network::mojom::URLLoaderClient>(std::move(client_remote))
+          ->OnComplete(
+              network::URLLoaderCompletionStatus(network::CorsErrorStatus(
+                  network::mojom::CorsError::kCorsDisabledScheme)));
+      return;
+    }
+
     auto head = network::mojom::URLResponseHead::New();
     head->request_start = head->response_start = base::TimeTicks::Now();
+    head->response_type = response_type;
     receiver_.Bind(std::move(loader));
     receiver_.set_disconnect_handler(base::BindOnce(
         &ContentURLLoader::OnMojoDisconnect, base::Unretained(this)));
diff --git a/content/browser/loader/file_url_loader_factory.cc b/content/browser/loader/file_url_loader_factory.cc
index 912a8b7..c550b8b 100644
--- a/content/browser/loader/file_url_loader_factory.cc
+++ b/content/browser/loader/file_url_loader_factory.cc
@@ -149,22 +149,6 @@
   }
 }
 
-network::mojom::FetchResponseType CalculateResponseType(
-    network::mojom::RequestMode mode,
-    bool is_allowed_access) {
-  // Though file:// is out of web standards, let's roughly follow the step 5 of
-  // https://fetch.spec.whatwg.org/#main-fetch.
-  if (is_allowed_access || mode == network::mojom::RequestMode::kNavigate ||
-      mode == network::mojom::RequestMode::kSameOrigin) {
-    return network::mojom::FetchResponseType::kBasic;
-  } else if (mode == network::mojom::RequestMode::kNoCors) {
-    return network::mojom::FetchResponseType::kOpaque;
-  } else {
-    DCHECK(network::cors::IsCorsEnabledRequestMode(mode)) << mode;
-    return network::mojom::FetchResponseType::kCors;
-  }
-}
-
 class FileURLDirectoryLoader
     : public network::mojom::URLLoader,
       public net::DirectoryLister::DirectoryListerDelegate {
@@ -841,7 +825,7 @@
   // check that takes --allow-file-access-from-files into account.
   // CORS is not available for the file scheme, but can be exceptionally
   // permitted by the access lists.
-  bool is_allowed_access =
+  bool is_request_considered_same_origin =
       base::CommandLine::ForCurrentProcess()->HasSwitch(
           switches::kDisableWebSecurity) ||
       (request.request_initiator &&
@@ -853,7 +837,8 @@
              network::cors::OriginAccessList::AccessState::kAllowed)));
 
   network::mojom::FetchResponseType response_type =
-      CalculateResponseType(request.mode, is_allowed_access);
+      network::cors::CalculateResponseType(request.mode,
+                                           is_request_considered_same_origin);
 
   CreateLoaderAndStartInternal(request, response_type, std::move(loader),
                                std::move(client));
diff --git a/services/network/public/cpp/cors/cors.cc b/services/network/public/cpp/cors/cors.cc
index 9cc1e6e..3c95e68 100644
--- a/services/network/public/cpp/cors/cors.cc
+++ b/services/network/public/cpp/cors/cors.cc
@@ -624,6 +624,21 @@
   }
 }
 
+mojom::FetchResponseType CalculateResponseType(
+    mojom::RequestMode mode,
+    bool is_request_considered_same_origin) {
+  if (is_request_considered_same_origin ||
+      mode == network::mojom::RequestMode::kNavigate ||
+      mode == network::mojom::RequestMode::kSameOrigin) {
+    return network::mojom::FetchResponseType::kBasic;
+  } else if (mode == network::mojom::RequestMode::kNoCors) {
+    return network::mojom::FetchResponseType::kOpaque;
+  } else {
+    DCHECK(network::cors::IsCorsEnabledRequestMode(mode)) << mode;
+    return network::mojom::FetchResponseType::kCors;
+  }
+}
+
 }  // namespace cors
 
 }  // namespace network
diff --git a/services/network/public/cpp/cors/cors.h b/services/network/public/cpp/cors/cors.h
index 736a01d..bd3e9ae 100644
--- a/services/network/public/cpp/cors/cors.h
+++ b/services/network/public/cpp/cors/cors.h
@@ -168,6 +168,19 @@
 bool CalculateCredentialsFlag(mojom::CredentialsMode credentials_mode,
                               mojom::FetchResponseType response_tainting);
 
+// TODO(toyoshim): Consider finding a more organized way to ensure adopting CORS
+// checks against all URLLoaderFactory and URLLoader inheritances.
+// Calculates mojom::FetchResponseType for non HTTP/HTTPS schemes those are out
+// of web standards. This adopts a simplified step 5 of
+// https://fetch.spec.whatwg.org/#main-fetch. |mode| is one of the
+// network::ResourceRequest to provide a CORS mode for the request.
+// |is_request_considered_same_origin| specifies if the request has a special
+// permission to bypass CORS checks.
+COMPONENT_EXPORT(NETWORK_CPP)
+mojom::FetchResponseType CalculateResponseType(
+    mojom::RequestMode mode,
+    bool is_request_considered_same_origin);
+
 }  // namespace cors
 
 }  // namespace network