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