Bug 1694679 - Skip CORS for moz-extension:-URLs r=ckerschb,mixedpuppy,necko-reviewers,dragana
authorRob Wu <rob@robwu.nl>
Fri, 09 Apr 2021 17:06:20 +0000
changeset 575290 6f298a911d9cc941b3eeca7958a6f413d5d1ab62
parent 575289 a6092057c08db07a6fae320a44b7cf0184a47474
child 575291 787c24b748c83b624c42ab1db9d22c9d9b086153
push id140633
push userrob@robwu.nl
push dateFri, 09 Apr 2021 17:51:48 +0000
treeherderautoland@6f298a911d9c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersckerschb, mixedpuppy, necko-reviewers, dragana
bugs1694679
milestone89.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1694679 - Skip CORS for moz-extension:-URLs r=ckerschb,mixedpuppy,necko-reviewers,dragana moz-extension:-URLs cannot be loaded by default, unless an extension explicitly lists the resource in web_accessible_resources. At that point, a URL is considered world-readable, and the load should succeed regardless of the requested CORS mode in the fetch/request. Differential Revision: https://phabricator.services.mozilla.com/D111016
netwerk/protocol/http/nsCORSListenerProxy.cpp
toolkit/components/extensions/test/xpcshell/test_ext_cors_mozextension.js
toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
--- a/netwerk/protocol/http/nsCORSListenerProxy.cpp
+++ b/netwerk/protocol/http/nsCORSListenerProxy.cpp
@@ -503,16 +503,25 @@ nsresult nsCORSListenerProxy::CheckReque
                         topChannel);
     }
     return status;
   }
 
   // Test that things worked on a HTTP level
   nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequest);
   if (!http) {
+    nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+    nsCOMPtr<nsIURI> uri;
+    NS_GetFinalChannelURI(channel, getter_AddRefs(uri));
+    if (uri && uri->SchemeIs("moz-extension")) {
+      // moz-extension:-URLs do not support CORS, but can universally be read
+      // if an extension lists the resource in web_accessible_resources.
+      // Access will be checked in UpdateChannel.
+      return NS_OK;
+    }
     LogBlockedRequest(aRequest, "CORSRequestNotHttp", nullptr,
                       nsILoadInfo::BLOCKING_REASON_CORSREQUESTNOTHTTP,
                       topChannel);
     return NS_ERROR_DOM_BAD_URI;
   }
 
   nsCOMPtr<nsILoadInfo> loadInfo = http->LoadInfo();
   if (loadInfo->GetServiceWorkerTaintingSynthesized()) {
@@ -875,16 +884,25 @@ nsresult nsCORSListenerProxy::UpdateChan
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (originalURI != uri) {
     rv = nsContentUtils::GetSecurityManager()->CheckLoadURIWithPrincipal(
         mRequestingPrincipal, originalURI, flags, loadInfo->GetInnerWindowID());
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
+  if (uri->SchemeIs("moz-extension")) {
+    // moz-extension:-URLs do not support CORS, but can universally be read
+    // if an extension lists the resource in web_accessible_resources.
+    // This is enforced via the CheckLoadURIWithPrincipal call above:
+    // moz-extension resources have the URI_DANGEROUS_TO_LOAD flag, unless
+    // listed in web_accessible_resources.
+    return NS_OK;
+  }
+
   if (!mHasBeenCrossSite &&
       NS_SUCCEEDED(mRequestingPrincipal->CheckMayLoad(uri, false)) &&
       (originalURI == uri ||
        NS_SUCCEEDED(mRequestingPrincipal->CheckMayLoad(originalURI, false)))) {
     return NS_OK;
   }
 
   // If the CSP directive 'upgrade-insecure-requests' is used or the HTTPS-Only
@@ -979,16 +997,19 @@ nsresult nsCORSListenerProxy::CheckPrefl
       loadInfo->GetIsPreflight()) {
     return NS_OK;
   }
 
   bool doPreflight = loadInfo->GetForcePreflight();
 
   nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aChannel);
   if (!http) {
+    // Note: A preflight is not needed for moz-extension:-requests either, but
+    // there is already a check for that in the caller of CheckPreflightNeeded,
+    // in UpdateChannel.
     LogBlockedRequest(aChannel, "CORSRequestNotHttp", nullptr,
                       nsILoadInfo::BLOCKING_REASON_CORSREQUESTNOTHTTP,
                       mHttpChannel);
     return NS_ERROR_DOM_BAD_URI;
   }
 
   nsAutoCString method;
   Unused << http->GetRequestMethod(method);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_cors_mozextension.js
@@ -0,0 +1,171 @@
+"use strict";
+
+const server = createHttpServer({
+  hosts: ["example.com", "x.example.com"],
+});
+server.registerPathHandler("/dummy", (req, res) => {
+  res.write("dummy");
+});
+server.registerPathHandler("/redir", (req, res) => {
+  res.setStatusLine(req.httpVersion, 302, "Found");
+  res.setHeader("Access-Control-Allow-Origin", "http://example.com");
+  res.setHeader("Access-Control-Allow-Credentials", "true");
+  res.setHeader("Location", new URLSearchParams(req.queryString).get("url"));
+});
+
+add_task(async function load_moz_extension_with_and_without_cors() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      web_accessible_resources: ["ok.js"],
+    },
+    files: {
+      "ok.js": "window.status = 'loaded';",
+      "deny.js": "window.status = 'unexpected load'",
+    },
+  });
+  await extension.startup();
+  const EXT_BASE_URL = `moz-extension://${extension.uuid}`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(
+    "http://example.com/dummy"
+  );
+  await contentPage.spawn(EXT_BASE_URL, async EXT_BASE_URL => {
+    const { document, window } = this.content;
+    async function checkScriptLoad({ setupScript, expectLoad, description }) {
+      const scriptElem = document.createElement("script");
+      setupScript(scriptElem);
+      return new Promise(resolve => {
+        window.status = "initial";
+        scriptElem.onload = () => {
+          Assert.equal(window.status, "loaded", "Script executed upon load");
+          Assert.ok(expectLoad, `Script loaded - ${description}`);
+          resolve();
+        };
+        scriptElem.onerror = () => {
+          Assert.equal(window.status, "initial", "not executed upon error");
+          Assert.ok(!expectLoad, `Script not loaded - ${description}`);
+          resolve();
+        };
+        document.head.append(scriptElem);
+      });
+    }
+
+    function sameOriginRedirectUrl(url) {
+      return `http://example.com/redir?url=` + encodeURIComponent(url);
+    }
+    function crossOriginRedirectUrl(url) {
+      return `http://x.example.com/redir?url=` + encodeURIComponent(url);
+    }
+
+    // Direct load of web-accessible extension script.
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = `${EXT_BASE_URL}/ok.js`;
+      },
+      expectLoad: true,
+      description: "web-accessible script, plain load",
+    });
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = `${EXT_BASE_URL}/ok.js`;
+        scriptElem.crossOrigin = "anonymous";
+      },
+      expectLoad: true,
+      description: "web-accessible script, cors",
+    });
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = `${EXT_BASE_URL}/ok.js`;
+        scriptElem.crossOrigin = "use-credentials";
+      },
+      expectLoad: true,
+      description: "web-accessible script, cors+credentials",
+    });
+
+    // Load of web-accessible extension scripts, after same-origin redirect.
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = sameOriginRedirectUrl(`${EXT_BASE_URL}/ok.js`);
+      },
+      expectLoad: true,
+      description: "same-origin redirect to web-accessible script, plain load",
+    });
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = sameOriginRedirectUrl(`${EXT_BASE_URL}/ok.js`);
+        scriptElem.crossOrigin = "anonymous";
+      },
+      expectLoad: true,
+      description: "same-origin redirect to web-accessible script, cors",
+    });
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = sameOriginRedirectUrl(`${EXT_BASE_URL}/ok.js`);
+        scriptElem.crossOrigin = "use-credentials";
+      },
+      expectLoad: true,
+      description:
+        "same-origin redirect to web-accessible script, cors+credentials",
+    });
+
+    // Load of web-accessible extension scripts, after cross-origin redirect.
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = crossOriginRedirectUrl(`${EXT_BASE_URL}/ok.js`);
+      },
+      expectLoad: true,
+      description: "cross-origin redirect to web-accessible script, plain load",
+    });
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = crossOriginRedirectUrl(`${EXT_BASE_URL}/ok.js`);
+        scriptElem.crossOrigin = "anonymous";
+      },
+      expectLoad: true,
+      description: "cross-origin redirect to web-accessible script, cors",
+    });
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = crossOriginRedirectUrl(`${EXT_BASE_URL}/ok.js`);
+        scriptElem.crossOrigin = "use-credentials";
+      },
+      expectLoad: true,
+      description:
+        "cross-origin redirect to web-accessible script, cors+credentials",
+    });
+
+    // Various loads of non-web-accessible extension script.
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = `${EXT_BASE_URL}/deny.js`;
+      },
+      expectLoad: false,
+      description: "non-accessible script, plain load",
+    });
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = `${EXT_BASE_URL}/deny.js`;
+        scriptElem.crossOrigin = "anonymous";
+      },
+      expectLoad: false,
+      description: "non-accessible script, cors",
+    });
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = sameOriginRedirectUrl(`${EXT_BASE_URL}/deny.js`);
+        scriptElem.crossOrigin = "anonymous";
+      },
+      expectLoad: false,
+      description: "same-origin redirect to non-accessible script, cors",
+    });
+    await checkScriptLoad({
+      setupScript(scriptElem) {
+        scriptElem.src = crossOriginRedirectUrl(`${EXT_BASE_URL}/deny.js`);
+        scriptElem.crossOrigin = "anonymous";
+      },
+      expectLoad: false,
+      description: "cross-origin redirect to non-accessible script, cors",
+    });
+  });
+  await contentPage.close();
+  await extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -66,16 +66,17 @@ skip-if = os == "android" # Bug 1700482
 [test_ext_contentscript_module_import.js]
 [test_ext_contentscript_restrictSchemes.js]
 [test_ext_contentscript_teardown.js]
 skip-if = tsan # Bug 1683730
 [test_ext_contentscript_unregister_during_loadContentScript.js]
 [test_ext_contentscript_xml_prettyprint.js]
 [test_ext_contextual_identities.js]
 skip-if = appname == "thunderbird" || os == "android" # Containers are not exposed to android.
+[test_ext_cors_mozextension.js]
 [test_ext_debugging_utils.js]
 [test_ext_dns.js]
 skip-if = socketprocess_networking || os == "android" # Android: Bug 1680132
 [test_ext_downloads.js]
 [test_ext_downloads_cookies.js]
 skip-if = os == "android" # downloads API needs to be implemented in GeckoView - bug 1538348
 [test_ext_downloads_download.js]
 skip-if = appname == "thunderbird" || os == "android" || tsan # tsan: bug 1612707