blob: 5122fd2930896b9d535d1daf78476bc89eb3dc3b [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/json/json_reader.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
#include "chrome/browser/extensions/api/permissions/permissions_api.h"
#include "chrome/browser/extensions/api/tabs/tabs_api.h"
#include "chrome/browser/extensions/chrome_content_browser_client_extensions_part.h"
#include "chrome/browser/extensions/extension_action_runner.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_function_test_utils.h"
#include "chrome/browser/extensions/extension_management_test_util.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/embedder_support/switches.h"
#include "components/metrics/content/subprocess_metrics_provider.h"
#include "components/policy/core/browser/browser_policy_connector.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/console_message.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/service_worker_context_observer.h"
#include "content/public/browser/shared_cors_origin_access_list.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/network_service_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/url_loader_interceptor.h"
#include "extensions/browser/browsertest_util.h"
#include "extensions/browser/permissions_manager.h"
#include "extensions/browser/url_loader_factory_manager.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/manifest_handlers/incognito_info.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/permissions_manager_waiter.h"
#include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "services/network/public/cpp/corb/corb_impl.h"
#include "services/network/public/cpp/cors/origin_access_list.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/mojom/network_context.mojom-shared.h"
#include "services/network/public/mojom/trust_tokens.mojom-shared.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace extensions {
namespace {
const char kCorsErrorWhenFetching[] = "error: TypeError: Failed to fetch";
// This is the public key of tools/origin_trials/eftest.key, used to validate
// origin trial tokens generated by tools/origin_trials/generate_token.py.
constexpr char kOriginTrialPublicKeyForTesting[] =
"dRCs+TocuKkocNKa0AtZ4awrt9XKH2SQCI6o4FY6BNA=";
std::string CreateFetchScript(
const GURL& resource,
absl::optional<base::Value> request_init = absl::nullopt) {
CHECK(request_init == absl::nullopt || request_init->is_dict());
const char kFetchScriptTemplate[] = R"(
fetch($1, $2)
.then(response => response.text())
.then(text => domAutomationController.send(text))
.catch(err => domAutomationController.send('error: ' + err));
)";
return content::JsReplace(kFetchScriptTemplate, resource,
request_init
? std::move(*request_init)
: base::Value(base::Value::Type::DICTIONARY));
}
std::string PopString(content::DOMMessageQueue* message_queue) {
std::string json;
EXPECT_TRUE(message_queue->WaitForMessage(&json));
absl::optional<base::Value> value =
base::JSONReader::Read(json, base::JSON_ALLOW_TRAILING_COMMAS);
EXPECT_TRUE(value->is_string());
return value->GetString();
}
} // namespace
using CORBAction = network::corb::CrossOriginReadBlocking::Action;
using ::testing::HasSubstr;
class CorbAndCorsExtensionTestBase : public ExtensionBrowserTest {
public:
CorbAndCorsExtensionTestBase() = default;
void SetUpDefaultCommandLine(base::CommandLine* command_line) override {
InProcessBrowserTest::SetUpDefaultCommandLine(command_line);
command_line->AppendSwitchASCII(embedder_support::kOriginTrialPublicKey,
kOriginTrialPublicKeyForTesting);
}
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
content::SetupCrossSiteRedirector(embedded_test_server());
}
protected:
TestExtensionDir dir_;
};
class ServiceWorkerConsoleObserver
: public content::ServiceWorkerContextObserver {
public:
explicit ServiceWorkerConsoleObserver(
content::BrowserContext* browser_context) {
content::StoragePartition* partition =
browser_context->GetDefaultStoragePartition();
scoped_observation_.Observe(partition->GetServiceWorkerContext());
}
~ServiceWorkerConsoleObserver() override = default;
ServiceWorkerConsoleObserver(const ServiceWorkerConsoleObserver&) = delete;
ServiceWorkerConsoleObserver& operator=(const ServiceWorkerConsoleObserver&) =
delete;
using Message = content::ConsoleMessage;
const std::vector<Message>& messages() const { return messages_; }
void WaitForMessages() { run_loop_.Run(); }
private:
// ServiceWorkerContextObserver:
void OnReportConsoleMessage(int64_t version_id,
const GURL& scope,
const Message& message) override {
messages_.push_back(message);
run_loop_.Quit();
}
base::RunLoop run_loop_;
std::vector<Message> messages_;
base::ScopedObservation<content::ServiceWorkerContext,
content::ServiceWorkerContextObserver>
scoped_observation_{this};
};
class CorbAndCorsExtensionBrowserTest : public CorbAndCorsExtensionTestBase {
public:
CorbAndCorsExtensionBrowserTest() = default;
CorbAndCorsExtensionBrowserTest(const CorbAndCorsExtensionBrowserTest&) =
delete;
CorbAndCorsExtensionBrowserTest& operator=(
const CorbAndCorsExtensionBrowserTest&) = delete;
void SetUpInProcessBrowserTestFixture() override {
policy_provider_.SetDefaultReturns(
/*is_initialization_complete_return=*/true,
/*is_first_policy_load_complete_return=*/true);
policy_provider_.SetAutoRefresh();
policy::BrowserPolicyConnector::SetPolicyProviderForTesting(
&policy_provider_);
}
const Extension* InstallExtension(
GURL resource_to_fetch_from_declarative_content_script = GURL()) {
bool use_declarative_content_script =
resource_to_fetch_from_declarative_content_script.is_valid();
const char kContentScriptManifestEntry[] = R"(
"content_scripts": [{
"all_frames": true,
"match_about_blank": true,
"matches": ["*://fetch-initiator.com/*"],
"js": ["content_script.js"]
}],
)";
const char kManifestTemplate[] = R"(
{
"name": "CrossOriginReadBlockingTest - Extension",
"version": "1.0",
"manifest_version": 2,
"permissions": [
"tabs",
"*://fetch-initiator.com/*",
"*://127.0.0.1/*",
"*://cross-site.com/*",
"*://*.subdomain.com/*",
"*://other-with-permission.com/*"
// This list intentionally does NOT include
// other-without-permission.com.
],
%s
"background": {"scripts": ["background_script.js"]},
"web_accessible_resources": [ "page.html" ]
} )";
dir_.WriteManifest(base::StringPrintf(
kManifestTemplate,
use_declarative_content_script ? kContentScriptManifestEntry : ""));
dir_.WriteFile(FILE_PATH_LITERAL("background_script.js"), "");
dir_.WriteFile(FILE_PATH_LITERAL("page.html"), "<body>Hello World!</body>");
if (use_declarative_content_script) {
dir_.WriteFile(
FILE_PATH_LITERAL("content_script.js"),
CreateFetchScript(resource_to_fetch_from_declarative_content_script));
}
extension_ =
LoadExtension(dir_.UnpackedPath(), {.allow_in_incognito = true});
DCHECK(extension_);
return extension_;
}
// Verifies that |console_observer| has captured a console message indicating
// that CORS has blocked a response.
//
// |console_observer| can be either
// - ServiceWorkerConsoleObserver (defined above in this file)
// or
// - content::WebContentsConsoleObserver
template <typename TConsoleObserver>
void VerifyFetchWasBlockedByCors(const TConsoleObserver& console_observer) {
using ConsoleMessage = typename TConsoleObserver::Message;
const std::vector<ConsoleMessage>& console_messages =
console_observer.messages();
std::vector<std::string> messages;
std::transform(console_messages.begin(), console_messages.end(),
std::back_inserter(messages),
[](const ConsoleMessage& console_message) {
return base::UTF16ToUTF8(console_message.message);
});
// We allow more than 1 console message, because the test might flakily see
// extra console messages - see https://crbug.com/1085629.
EXPECT_THAT(messages, testing::Contains(testing::HasSubstr(
"has been blocked by CORS policy")));
}
void VerifyFetchWasBlockedByCorb(const base::HistogramTester& histograms) {
// Make sure that histograms logged in other processes (e.g. in
// NetworkService process) get synced.
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
histograms.ExpectBucketCount("SiteIsolation.XSD.Browser.Action",
CORBAction::kResponseStarted, 1);
histograms.ExpectBucketCount("SiteIsolation.XSD.Browser.Action",
CORBAction::kBlockedWithoutSniffing, 1);
}
void VerifyFetchWasAllowedByCorb(const base::HistogramTester& histograms,
bool expecting_sniffing = false) {
// Make sure that histograms logged in other processes (e.g. in
// NetworkService process) get synced.
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
histograms.ExpectBucketCount("SiteIsolation.XSD.Browser.Action",
CORBAction::kResponseStarted, 1);
histograms.ExpectBucketCount("SiteIsolation.XSD.Browser.Action",
expecting_sniffing
? CORBAction::kAllowedAfterSniffing
: CORBAction::kAllowedWithoutSniffing,
1);
}
void VerifyCorbWasDisabled(const base::HistogramTester& histograms) {
// Make sure that histograms logged in other processes (e.g. in
// NetworkService process) get synced.
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
EXPECT_THAT(histograms.GetTotalCountsForPrefix("SiteIsolation.XSD.Browser"),
::testing::IsEmpty());
}
// Verifies that fetching a CORB-eligible resource from a content script will
// be blocked by CORS.
//
// This verification helper might not work for non-CORB-eligible resources
// like MIME types not covered by CORB (e.g. application/octet-stream) or
// same-origin responses.
void VerifyCorbEligibleFetchFromContentScript(
const base::HistogramTester& histograms,
const content::WebContentsConsoleObserver& console_observer,
const std::string& actual_fetch_result,
const std::string& expected_fetch_result) {
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
// Verify the fetch was blocked by CORS.
EXPECT_EQ(kCorsErrorWhenFetching, actual_fetch_result);
VerifyFetchWasBlockedByCors(console_observer);
// No verification if the request was blocked by CORB, because
// 1) once request_initiator is trustworthy, CORB should only
// apply to no-cors requests
// 2) some CORS-blocked requests may not reach CORB/response-started
// stage at all (e.g. if CORS blocks a redirect).
// TODO(lukasza): Verify that the request was made in CORS mode (e.g.
// included an Origin header).
}
void VerifyNonCorbElligibleFetchFromContentScript(
const base::HistogramTester& histograms,
const content::WebContentsConsoleObserver& console_observer,
const std::string& actual_fetch_result,
const std::string& expected_fetch_result_prefix) {
// Verify that CORB allowed the response.
VerifyFetchWasAllowedByCorb(histograms, false /* expecting_sniffing */);
// Verify that the response body was blocked by CORS.
EXPECT_EQ(kCorsErrorWhenFetching, actual_fetch_result);
VerifyFetchWasBlockedByCors(console_observer);
}
content::WebContents* active_web_contents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
const Extension* InstallExtensionWithManifest(base::StringPiece manifest) {
dir_.WriteManifest(manifest);
dir_.WriteFile(FILE_PATH_LITERAL("background_script.js"), "");
dir_.WriteFile(FILE_PATH_LITERAL("page.html"), "");
extension_ = LoadExtension(dir_.UnpackedPath());
return extension_;
}
const Extension* InstallExtensionWithPermissionToAllUrls(
bool enable_file_access = false) {
const char kManifestTemplate[] = R"(
{
"name": "CrossOriginReadBlockingTest - Extension/AllUrls",
"version": "1.0",
"manifest_version": 2,
"permissions": [ "tabs", "<all_urls>" ],
"background": {"scripts": ["background_script.js"]}
} )";
dir_.WriteManifest(kManifestTemplate);
dir_.WriteFile(FILE_PATH_LITERAL("background_script.js"), "");
extension_ = LoadExtension(dir_.UnpackedPath(),
{.allow_file_access = enable_file_access});
DCHECK(extension_);
return extension_;
}
bool RegisterServiceWorkerForExtension(
const std::string& service_worker_script) {
const char kServiceWorkerPath[] = "service_worker.js";
dir_.WriteFile(base::FilePath::FromASCII(kServiceWorkerPath).value(),
service_worker_script);
const char kRegistrationScript[] = R"(
navigator.serviceWorker.register($1).then(function() {
// Wait until the service worker is active.
return navigator.serviceWorker.ready;
}).then(function(r) {
window.domAutomationController.send('SUCCESS');
}).catch(function(err) {
window.domAutomationController.send('ERROR: ' + err.message);
}); )";
std::string registration_script =
content::JsReplace(kRegistrationScript, kServiceWorkerPath);
std::string result = browsertest_util::ExecuteScriptInBackgroundPage(
browser()->profile(), extension_->id(), registration_script);
if (result != "SUCCESS") {
ADD_FAILURE() << "Failed to register the service worker: " << result;
return false;
}
return !::testing::Test::HasFailure();
}
// Injects (into |web_contents|) a content_script that performs a fetch of
// |url|. Returns the body of the response.
//
// The method below uses "programmatic" (rather than "declarative") way to
// inject a content script, but the behavior and permissions of the conecnt
// script should be the same in both cases. See also
// https://developer.chrome.com/extensions/content_scripts#programmatic.
std::string FetchViaContentScript(const GURL& url,
content::WebContents* web_contents) {
return FetchHelper(
url, web_contents,
base::BindOnce(&CorbAndCorsExtensionBrowserTest::ExecuteContentScript,
base::Unretained(this), base::Unretained(web_contents)));
}
// Performs a fetch of |url| from the background page of the test extension.
// Returns the body of the response.
std::string FetchViaBackgroundPage(const GURL& url,
const Extension* extension,
Browser* browser) {
content::WebContents* background_web_contents =
ProcessManager::Get(browser->profile())
->GetBackgroundHostForExtension(extension->id())
->host_contents();
return FetchViaFrame(url, background_web_contents);
}
std::string FetchViaBackgroundPage(const GURL& url) {
return FetchViaBackgroundPage(url, extension_, browser());
}
// Performs a fetch of `url` from `execution_target` (directly, without going
// through content scripts). Returns the body of the response.
std::string FetchViaFrame(
const GURL& url,
const content::ToRenderFrameHost& execution_target) {
return FetchHelper(
url,
content::WebContents::FromRenderFrameHost(
execution_target.render_frame_host()),
base::BindOnce(&CorbAndCorsExtensionBrowserTest::ExecuteRegularScript,
base::Unretained(this),
execution_target.render_frame_host()));
}
// Performs a fetch of |url| from a srcdoc subframe added to |parent_frame|
// and executing a script via <script> tag. Returns the body of the response.
std::string FetchViaSrcDocFrame(GURL url,
content::RenderFrameHost* parent_frame) {
return FetchHelper(
url, content::WebContents::FromRenderFrameHost(parent_frame),
base::BindOnce(&CorbAndCorsExtensionBrowserTest::ExecuteInSrcDocFrame,
base::Unretained(this), base::Unretained(parent_frame)));
}
GURL GetExtensionResource(const std::string& relative_path) {
return extension_->GetResourceURL(relative_path);
}
url::Origin GetExtensionOrigin() { return extension_->origin(); }
GURL GetTestPageUrl(const std::string& hostname) {
// Using the page below avoids a network fetch of /favicon.ico which helps
// avoid extra synchronization hassles in the tests.
return embedded_test_server()->GetURL(
hostname, "/favicon/title1_with_data_uri_icon.html");
}
const Extension* extension() { return extension_; }
// Asks the test |extension_| to inject |content_script| into |web_contents|.
//
// This is an implementation of FetchCallback.
// Returns true if the content script execution started succeessfully.
bool ExecuteContentScript(content::WebContents* web_contents,
const std::string& content_script) {
int tab_id = ExtensionTabUtil::GetTabId(web_contents);
std::string background_script = content::JsReplace(
"chrome.tabs.executeScript($1, { code: $2 });", tab_id, content_script);
return browsertest_util::ExecuteScriptInBackgroundPageNoWait(
browser()->profile(), extension_->id(), background_script);
}
protected:
testing::NiceMock<policy::MockConfigurationPolicyProvider> policy_provider_;
private:
// Executes |regular_script| in |web_contents|.
//
// This is an implementation of FetchCallback.
// Returns true if the script execution started succeessfully.
bool ExecuteRegularScript(content::RenderFrameHost* frame,
const std::string& regular_script) {
content::ExecuteScriptAsync(frame, regular_script);
// Report artificial success to meet FetchCallback's requirements.
return true;
}
// Injects into |parent_frame| an "srcdoc" subframe that contains/executes
// |script_to_run_in_subframe| via <script> tag.
//
// This function is useful to exercise a scenario when a <script> tag may
// execute before the browser gets a chance to see the a frame/navigation
// commit is happening.
//
// This is an implementation of FetchCallback.
// Returns true if the script execution started succeessfully.
bool ExecuteInSrcDocFrame(content::RenderFrameHost* parent_frame,
const std::string& script_to_run_in_subframe) {
static int sequence_id = 0;
sequence_id++;
std::string filename =
base::StringPrintf("srcdoc_script_%d.js", sequence_id);
dir_.WriteFile(base::FilePath::FromUTF8Unsafe(filename).value(),
script_to_run_in_subframe);
// Using <script src=...></script> instead of <script>...</script> to avoid
// extensions CSP which forbids inline scripts.
const char kScriptTemplate[] = R"(
var subframe = document.createElement('iframe');
subframe.srcdoc = '<script src=' + $1 + '></script>';
document.body.appendChild(subframe); )";
std::string subframe_injection_script =
content::JsReplace(kScriptTemplate, filename);
content::ExecuteScriptAsync(parent_frame, subframe_injection_script);
// Report artificial success to meet FetchCallback's requirements.
return true;
}
// FetchCallback represents a function that executes |fetch_script|.
//
// |fetch_script| will include calls to |domAutomationController.send| and
// therefore instances of FetchCallback should not inject their own calls to
// |domAutomationController.send| (e.g. this constraint rules out
// browsertest_util::ExecuteScriptInBackgroundPage and/or
// content::ExecuteScript).
//
// The function should return true if script execution started successfully.
//
// Currently used "implementations":
// - CorbAndCorsExtensionBrowserTest::ExecuteContentScript(web_contents)
// - CorbAndCorsExtensionBrowserTest::ExecuteRegularScript(web_contents)
// - browsertest_util::ExecuteScriptInBackgroundPageNoWait(profile, ext_id)
using FetchCallback =
base::OnceCallback<bool(const std::string& fetch_script)>;
// Returns response body of a fetch of |url| initiated via |fetch_callback|.
std::string FetchHelper(const GURL& url,
content::WebContents* web_contents,
FetchCallback fetch_callback) {
content::DOMMessageQueue message_queue(web_contents);
// Inject a content script that performs a cross-origin fetch to
// cross-site.com.
EXPECT_TRUE(std::move(fetch_callback).Run(CreateFetchScript(url)));
// Wait until the message comes back and extract result from the message.
return PopString(&message_queue);
}
raw_ptr<const Extension, DanglingUntriaged> extension_ = nullptr;
};
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromDeclarativeContentScript_NoSniffXml) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
ASSERT_TRUE(InstallExtension(cross_site_resource));
// Test case #1: Declarative script injected after a browser-initiated
// navigation of the main frame.
{
// Monitor CORB behavior + result of the fetch.
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
content::DOMMessageQueue message_queue(active_web_contents());
// Navigate to a fetch-initiator.com page - this should trigger execution of
// the |content_script| declared in the extension manifest.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
EXPECT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
EXPECT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Extract results of the fetch done in the declarative content script.
std::string fetch_result = PopString(&message_queue);
// Verify whether the fetch worked or not (expectations differ depending on
// various factors - see the body of
// VerifyCorbEligibleFetchFromContentScript).
VerifyCorbEligibleFetchFromContentScript(
histograms, console_observer, fetch_result, "nosniff.xml - body\n");
}
// Test case #2: Declarative script injected after a renderer-initiated
// creation of an about:blank frame.
{
// Monitor CORB behavior + result of the fetch.
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
content::DOMMessageQueue message_queue(active_web_contents());
// Inject an about:blank subframe - this should trigger execution of the
// |content_script| declared in the extension manifest.
const char kBlankSubframeInjectionScript[] = R"(
var subframe = document.createElement('iframe');
document.body.appendChild(subframe); )";
content::ExecuteScriptAsync(active_web_contents(),
kBlankSubframeInjectionScript);
// Extract results of the fetch done in the declarative content script.
std::string fetch_result = PopString(&message_queue);
// Verify whether the fetch worked or not (expectations differ depending on
// various factors - see the body of
// VerifyCorbEligibleFetchFromContentScript).
VerifyCorbEligibleFetchFromContentScript(
histograms, console_observer, fetch_result, "nosniff.xml - body\n");
}
}
// Test that verifies the current, baked-in (but not necessarily desirable
// behavior) where a content script injected by an extension can bypass
// CORS (and CORB) for any hosts the extension has access to.
// See also https://crbug.com/846346.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromProgrammaticContentScript_NoSniffXml) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a cross-origin fetch to
// cross-site.com.
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
std::string fetch_result =
FetchViaContentScript(cross_site_resource, active_web_contents());
// Verify whether the fetch worked or not (expectations differ depending on
// various factors - see the body of
// VerifyCorbEligibleFetchFromContentScript).
VerifyCorbEligibleFetchFromContentScript(
histograms, console_observer, fetch_result, "nosniff.xml - body\n");
}
// Tests that extension permission to bypass CORS is revoked after the extension
// is unloaded. See also https://crbug.com/843381.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromProgrammaticContentScript_UnloadedExtension) {
ASSERT_TRUE(embedded_test_server()->Start());
const extensions::Extension* extension = InstallExtension();
ASSERT_TRUE(extension);
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that adds a link that initiates a fetch from
// cross-site.com.
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
{
const char kNewButtonScriptTemplate[] = R"(
function startFetch() {
%s
}
var link = document.createElement('a');
link.href = '#foo';
link.addEventListener('click', function() {
startFetch();
});
link.id = 'fetch-button';
link.innerText = 'start-fetch';
document.body.appendChild(link);
domAutomationController.send('READY');
)";
content::DOMMessageQueue queue(active_web_contents());
ASSERT_TRUE(ExecuteContentScript(
active_web_contents(),
base::StringPrintf(kNewButtonScriptTemplate,
CreateFetchScript(cross_site_resource).c_str())));
ASSERT_EQ("READY", PopString(&queue));
}
// Click the button - the fetch should be blocked.
//
// Clicking the button will execute the 'click' handler belonging to the
// content script (i.e. the `startFetch` method defined in the
// kNewButtonScriptTemplate above). Directly executing the script via
// content::ExecuteScript would have executed the script in the main world
// (which is not what we want).
const char kFetchInitiatingScript[] = R"(
document.getElementById('fetch-button').click();
)";
{
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
content::DOMMessageQueue queue(active_web_contents());
content::ExecuteScriptAsync(active_web_contents(), kFetchInitiatingScript);
std::string fetch_result = PopString(&queue);
VerifyCorbEligibleFetchFromContentScript(
histograms, console_observer, fetch_result, "nosniff.xml - body\n");
}
// Unload the extension and try fetching again. The content script should
// still be present and work, but after the extension is unloaded, the fetch
// should always fail. See also https://crbug.com/843381.
extension_service()->DisableExtension(extension->id(),
disable_reason::DISABLE_USER_ACTION);
EXPECT_FALSE(ExtensionRegistry::Get(profile())->enabled_extensions().GetByID(
extension->id()));
{
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
content::DOMMessageQueue queue(active_web_contents());
content::ExecuteScriptAsync(active_web_contents(), kFetchInitiatingScript);
std::string fetch_result = PopString(&queue);
// Verify that the fetch was blocked by CORS. (CORB only applies to
// `no-cors` requests.)
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
VerifyFetchWasBlockedByCors(console_observer);
VerifyFetchWasAllowedByCorb(histograms, false /* expecting_sniffing */);
}
}
// Test that <all_urls> permission does not apply to hosts blocked by policy.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
ContentScriptVsHostBlockedByPolicy_NoSniffXml) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtensionWithPermissionToAllUrls());
{
ExtensionManagementPolicyUpdater pref(&policy_provider_);
pref.AddPolicyBlockedHost("*", "*://*.example.com");
pref.AddPolicyAllowedHost("*", "*://public.example.com");
}
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Test fetch from a host allowed by the policy (and allowed by the extension
// permissions).
{
SCOPED_TRACE(::testing::Message() << "Allowed by policy");
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
GURL cross_site_resource(
embedded_test_server()->GetURL("public.example.com", "/nosniff.xml"));
std::string fetch_result =
FetchViaContentScript(cross_site_resource, active_web_contents());
// Verify whether the fetch worked or not (expectations differ depending on
// various factors - see the body of
// VerifyCorbEligibleFetchFromContentScript).
VerifyCorbEligibleFetchFromContentScript(
histograms, console_observer, fetch_result, "nosniff.xml - body\n");
}
// Test fetch from a host blocked by the policy (and allowed by the extension
// permissions).
{
SCOPED_TRACE(::testing::Message() << "Blocked by policy");
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
GURL cross_site_resource(
embedded_test_server()->GetURL("example.com", "/nosniff.xml"));
std::string fetch_result =
FetchViaContentScript(cross_site_resource, active_web_contents());
// Verify that the fetch was blocked by CORS. (CORB only applies to
// `no-cors` requests.)
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
VerifyFetchWasBlockedByCors(console_observer);
VerifyFetchWasAllowedByCorb(histograms, false /* expecting_sniffing */);
}
}
// Test that <all_urls> permission does not apply to hosts blocked by policy.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
ContentScriptVsHostBlockedByPolicy_AllowedTextResource) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtensionWithPermissionToAllUrls());
{
ExtensionManagementPolicyUpdater pref(&policy_provider_);
pref.AddPolicyBlockedHost("*", "*://*.example.com");
pref.AddPolicyAllowedHost("*", "*://public.example.com");
}
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Test fetch from a host allowed by the policy (and allowed by the extension
// permissions).
{
SCOPED_TRACE(::testing::Message() << "Allowed by policy");
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
GURL cross_site_resource(embedded_test_server()->GetURL(
"public.example.com", "/save_page/text.txt"));
std::string fetch_result =
FetchViaContentScript(cross_site_resource, active_web_contents());
// Verify that the fetch was allowed by CORB. CORS expectations differ
// depending on exact scenario.
VerifyNonCorbElligibleFetchFromContentScript(
histograms, console_observer, fetch_result,
"text-object.txt: ae52dd09-9746-4b7e-86a6-6ada5e2680c2");
}
// Test fetch from a host blocked by the policy (and allowed by the extension
// permissions).
{
SCOPED_TRACE(::testing::Message() << "Blocked by policy");
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
GURL cross_site_resource(
embedded_test_server()->GetURL("example.com", "/save_page/text.txt"));
std::string fetch_result =
FetchViaContentScript(cross_site_resource, active_web_contents());
// Verify that the fetch was blocked by CORS.
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
VerifyFetchWasAllowedByCorb(histograms, false /* expecting_sniffing */);
VerifyFetchWasBlockedByCors(console_observer);
}
}
class CorbAndCorsUserHostRestrictionsBrowserTest
: public CorbAndCorsExtensionBrowserTest {
public:
CorbAndCorsUserHostRestrictionsBrowserTest() {
scoped_feature_list_.InitAndEnableFeature(
extensions_features::kExtensionsMenuAccessControl);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(CorbAndCorsUserHostRestrictionsBrowserTest,
PolicyVsUserHostRestrictions) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtensionWithPermissionToAllUrls());
{
ExtensionManagementPolicyUpdater pref(&policy_provider_);
pref.AddPolicyBlockedHost("*", "*://*.example.com");
pref.AddPolicyAllowedHost("*", "*://public.example.com");
}
GURL policy_allowed_resource =
embedded_test_server()->GetURL("public.example.com", "/nosniff.xml");
GURL policy_restricted_resource =
embedded_test_server()->GetURL("restricted.example.com", "/nosniff.xml");
// Now, add user settings that are in the inverse of the policy ones:
// Blocked on public.example.com, but allowed on restricted.example.com.
PermissionsManager* permissions_manager = PermissionsManager::Get(profile());
{
PermissionsManagerWaiter waiter(permissions_manager);
permissions_manager->AddUserRestrictedSite(
url::Origin::Create(policy_allowed_resource));
waiter.WaitForUserPermissionsSettingsChange();
}
{
PermissionsManagerWaiter waiter(permissions_manager);
permissions_manager->AddUserPermittedSite(
url::Origin::Create(policy_restricted_resource));
waiter.WaitForUserPermissionsSettingsChange();
}
{
std::string fetch_result = FetchViaBackgroundPage(policy_allowed_resource);
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
{
std::string fetch_result =
FetchViaBackgroundPage(policy_restricted_resource);
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
}
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromProgrammaticContentScript_PermissionToAllUrls) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtensionWithPermissionToAllUrls());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a cross-origin fetch to
// cross-site.com.
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
std::string fetch_result =
FetchViaContentScript(cross_site_resource, active_web_contents());
// Verify whether the fetch worked or not (expectations differ depending on
// various factors - see the body of
// VerifyCorbEligibleFetchFromContentScript).
VerifyCorbEligibleFetchFromContentScript(
histograms, console_observer, fetch_result, "nosniff.xml - body\n");
}
// Verification that granting file access to extensions doesn't relax CORS in
// case of requests to file: URLs (even from content scripts of extensions with
// <all_urls> permission). See also https://crbug.com/1049604#c14.
IN_PROC_BROWSER_TEST_F(
CorbAndCorsExtensionBrowserTest,
FromProgrammaticContentScript_PermissionToAllUrls_FileUrls) {
// Install the extension and verify that the extension has access to file URLs
// (<all_urls> permission is not sufficient - the extension has to be
// additionally granted file access by setting LoadOptions.allow_file_access
// to true in ExtensionBrowserTest::LoadExtension).
const Extension* extension =
InstallExtensionWithPermissionToAllUrls(/*enable_file_access=*/true);
ASSERT_TRUE(extension);
ASSERT_TRUE(util::AllowFileAccess(
extension->id(), active_web_contents()->GetBrowserContext()));
// Gather the test URLs.
GURL page_url = ui_test_utils::GetTestUrl(
base::FilePath(), base::FilePath(FILE_PATH_LITERAL("title1.html")));
GURL same_dir_resource = ui_test_utils::GetTestUrl(
base::FilePath(), base::FilePath(FILE_PATH_LITERAL("title2.html")));
ASSERT_EQ(url::kFileScheme, page_url.scheme());
ASSERT_EQ(url::kFileScheme, same_dir_resource.scheme());
// Navigate to a file:// test page.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
const char kScriptTemplate[] = R"(
const url = $1;
const xhr = new XMLHttpRequest();
xhr.onload = () => domAutomationController.send(xhr.responseText);
xhr.onerror = () => domAutomationController.send('XHR ERROR');
xhr.open("GET", url);
xhr.send()
)";
std::string script = content::JsReplace(kScriptTemplate, same_dir_resource);
// Sanity check: file: -> file: XHR (no extensions involved) should be blocked
// by CORS equivalent inside FileURLLoaderFactory. (All file: URLs are
// treated as an opaque origin, so all such XHRs would be considered
// cross-origin.)
{
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
content::DOMMessageQueue queue(active_web_contents());
ExecuteScriptAsync(active_web_contents(), script);
std::string xhr_result = PopString(&queue);
// Verify that the XHR was blocked by CORS-equivalent in
// FileURLLoaderFactory.
EXPECT_EQ("XHR ERROR", xhr_result);
VerifyFetchWasBlockedByCors(console_observer);
// CORB is not used from FileURLLoaderFactory - verify that no CORB UMAs
// have been logged.
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
EXPECT_EQ(
0u,
histograms.GetTotalCountsForPrefix("SiteIsolation.XSD.Browser").size());
}
// Inject a content script that performs a cross-origin XHR from another file:
// URL (all file: URLs are treated as an opaque origin, so all such XHRs would
// be considered cross-origin).
//
// The ability to inject content scripts into file: URLs comes from a
// combination of <all_urls> and granting file access to the extension.
//
// The script below uses the XMLHttpRequest API, rather than fetch API,
// because the fetch API doesn't support file: requests currently
// (see https://crbug.com/1051594#c9 and https://crbug.com/1051597#c19).
{
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
content::DOMMessageQueue queue(active_web_contents());
ExecuteContentScript(active_web_contents(), script);
std::string xhr_result = PopString(&queue);
// Verify that the XHR was blocked by CORS-equivalent in
// FileURLLoaderFactory (even though the extension has <all_urls> permission
// and was granted file access).
EXPECT_EQ("XHR ERROR", xhr_result);
VerifyFetchWasBlockedByCors(console_observer);
// CORB is not used from FileURLLoaderFactory - verify that no CORB UMAs
// have been logged.
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
EXPECT_EQ(
0u,
histograms.GetTotalCountsForPrefix("SiteIsolation.XSD.Browser").size());
}
}
// Coverage of *.subdomain.com extension permissions for CORB-eligible fetches
// (via nosniff.xml).
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromProgrammaticContentScript_SubdomainPermissions) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Verify behavior for fetching URLs covered by extension permissions.
GURL kAllowedUrls[] = {
embedded_test_server()->GetURL("subdomain.com", "/nosniff.xml"),
embedded_test_server()->GetURL("foo.subdomain.com", "/nosniff.xml"),
};
for (const GURL& allowed_url : kAllowedUrls) {
SCOPED_TRACE(::testing::Message() << "allowed_url = " << allowed_url);
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
std::string fetch_result =
FetchViaContentScript(allowed_url, active_web_contents());
// Verify whether the fetch worked or not (expectations differ depending on
// various factors - see the body of
// VerifyCorbEligibleFetchFromContentScript).
VerifyCorbEligibleFetchFromContentScript(
histograms, console_observer, fetch_result, "nosniff.xml - body\n");
}
}
// Test that verifies the current, baked-in (but not necessarily desirable
// behavior) where a content script injected by an extension can bypass
// CORS (and CORB) for any hosts the extension has access to.
// See also https://crbug.com/1034408 and https://crbug.com/846346.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromProgrammaticContentScript_RedirectToNoSniffXml) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a cross-origin fetch to
// cross-site.com.
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
GURL redirecting_url(embedded_test_server()->GetURL(
"other-with-permission.com",
std::string("/server-redirect?") + cross_site_resource.spec()));
std::string fetch_result =
FetchViaContentScript(redirecting_url, active_web_contents());
// Verify whether the fetch worked or not (expectations differ depending on
// various factors - see the body of
// VerifyCorbEligibleFetchFromContentScript).
VerifyCorbEligibleFetchFromContentScript(
histograms, console_observer, fetch_result, "nosniff.xml - body\n");
}
// Test that verifies CORS-allowed fetches work for targets that are not
// covered by the extension permissions.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
ContentScript_CorsAllowedByServer_NoPermissionToTarget) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a cross-origin fetch to
// other-without-permission.com.
base::HistogramTester histograms;
GURL cross_site_resource(embedded_test_server()->GetURL(
"other-without-permission.com", "/cors-ok.txt"));
std::string fetch_result =
FetchViaContentScript(cross_site_resource, active_web_contents());
// Verify that the fetch succeeded (because of the server's
// Access-Control-Allow-Origin response header).
EXPECT_EQ("cors-ok.txt - body\n", fetch_result);
VerifyFetchWasAllowedByCorb(histograms);
}
// Test that verifies that CORS blocks non-CORB-eligible fetches for targets
// that are not covered by the extension permissions.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
ContentScript_CorsIgnoredByServer_NoPermissionToTarget) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a cross-origin fetch to
// other-without-permission.com.
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
GURL cross_site_resource(embedded_test_server()->GetURL(
"other-without-permission.com", "/save_page/text.txt"));
std::string fetch_result =
FetchViaContentScript(cross_site_resource, active_web_contents());
// Verify that the fetch was blocked by CORS (because the extension has no
// permission to the target + server didn't reply with
// Access-Control-Allow-Origin response header).
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
VerifyFetchWasBlockedByCors(console_observer);
// Verify that the fetch was allowed by CORB (because CORB doesn't apply to
// CORS requests).
VerifyFetchWasAllowedByCorb(histograms, false /* expecting_sniffing */);
}
// Tests that same-origin fetches (same-origin relative to the webpage the
// content script is injected into) are allowed. See also
// https://crbug.com/918660.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromProgrammaticContentScript_SameOrigin) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a same-origin fetch to
// fetch-initiator.com.
base::HistogramTester histograms;
GURL same_origin_resource(
embedded_test_server()->GetURL("fetch-initiator.com", "/nosniff.xml"));
std::string fetch_result =
FetchViaContentScript(same_origin_resource, active_web_contents());
// Verify that no blocking occurred.
EXPECT_THAT(fetch_result, ::testing::StartsWith("nosniff.xml - body"));
VerifyFetchWasAllowedByCorb(histograms, false /* expecting_sniffing */);
}
// Test that responses that would have been allowed by CORB anyway are not
// reported to LogInitiatorSchemeBypassingDocumentBlocking.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromProgrammaticContentScript_AllowedTextResource) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a cross-origin fetch to
// cross-site.com.
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/save_page/text.txt"));
std::string fetch_result =
FetchViaContentScript(cross_site_resource, active_web_contents());
// Verify that the fetch was allowed by CORB. CORS expectations differ
// depending on exact scenario.
VerifyNonCorbElligibleFetchFromContentScript(
histograms, console_observer, fetch_result,
"text-object.txt: ae52dd09-9746-4b7e-86a6-6ada5e2680c2");
}
// The trust-token-redemption Permissions Policy feature, which is enabled by
// default, is required in order to execute a Trust Tokens
// (https://github.com/wicg/trust-token-api) redemption operation alongside a
// subresource request. To enforce this requirement, the browser binds the
// feature's value to a frame's subresource loader.
//
// Ensure that it is being propagated correctly for by verifying that a content
// script can execute a redemption operation.
//
// (Specifically, this makes sure RFHI is passing the correct factory
// parameter to URLLoaderFactoryParamsHelper::CreateForIsolatedWorld.)
class TrustTokenExtensionBrowserTest : public CorbAndCorsExtensionBrowserTest {
public:
TrustTokenExtensionBrowserTest() {
scoped_feature_list_.InitAndEnableFeature(
network::features::kPrivateStateTokens);
}
protected:
base::test::ScopedFeatureList scoped_feature_list_;
};
// TODO(crbug.com/1315215): Have trust tokens handle the existence, or not, of
// PrivacySandboxSettings3.
IN_PROC_BROWSER_TEST_F(
TrustTokenExtensionBrowserTest,
DISABLED_FromProgrammaticContentScript_TrustTokenRedemptionAllowed) {
// Trust Tokens operations only work on secure origins - set up a https test
// server to help with this. One alternative would be using a localhost URL
// from |embedded_test_server|, but this would require modifying the extension
// manifest in InstallExtension.
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.AddDefaultHandlers(GetChromeTestDataDir());
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK);
ASSERT_TRUE(https_server.Start());
// Load the test extension.
ASSERT_TRUE(InstallExtension());
GURL page_url = https_server.GetURL("/title1.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
// This doesn't need to exist; we expect the fetch to fail during precondition
// checking.
GURL resource("/fake-trust-token-page");
{
content::DOMMessageQueue message_queue(active_web_contents());
base::Value request_init(base::Value::Type::DICTIONARY);
request_init.SetStringPath("trustToken.type", "token-redemption");
EXPECT_TRUE(ExecuteContentScript(
active_web_contents(),
CreateFetchScript(resource, std::move(request_init))));
// The operation should fail because the Trust Tokens operation failed (we
// didn't set up enough Trust Tokens state for it to execute), not because
// the operation was forbidden (which would trigger a TypeError).
EXPECT_THAT(PopString(&message_queue), HasSubstr("InvalidStateError"));
}
// Make sure the permission propagates correctly after a network service
// crash.
if (!content::IsOutOfProcessNetworkService())
return;
SimulateNetworkServiceCrash();
active_web_contents()
->GetPrimaryMainFrame()
->FlushNetworkAndNavigationInterfacesForTesting();
{
content::DOMMessageQueue message_queue(active_web_contents());
base::Value request_init(base::Value::Type::DICTIONARY);
request_init.SetStringPath("trustToken.type", "token-redemption");
EXPECT_TRUE(ExecuteContentScript(
active_web_contents(),
CreateFetchScript(resource, std::move(request_init))));
// The operation should fail because the Trust Tokens operation failed (we
// didn't set up enough Trust Tokens state for it to execute), not because
// the operation was forbidden (which would trigger a TypeError).
EXPECT_THAT(PopString(&message_queue), HasSubstr("InvalidStateError"));
}
}
// Coverage of *.subdomain.com extension permissions for non-CORB eligible
// fetches (via save_page/text.txt).
IN_PROC_BROWSER_TEST_F(
CorbAndCorsExtensionBrowserTest,
FromProgrammaticContentScript_AllowedTextResource_SubdomainPermissions) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Verify behavior for fetching URLs covered by extension permissions.
GURL kAllowedUrls[] = {
embedded_test_server()->GetURL("subdomain.com", "/save_page/text.txt"),
embedded_test_server()->GetURL("x.subdomain.com", "/save_page/text.txt"),
};
for (const GURL& allowed_url : kAllowedUrls) {
SCOPED_TRACE(::testing::Message() << "allowed_url = " << allowed_url);
// Inject a content script that performs a cross-origin fetch to
// cross-site.com.
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
std::string fetch_result =
FetchViaContentScript(allowed_url, active_web_contents());
// Verify that CORB sniffing allowed the response.
VerifyNonCorbElligibleFetchFromContentScript(
histograms, console_observer, fetch_result,
"text-object.txt: ae52dd09-9746-4b7e-86a6-6ada5e2680c2");
}
}
// Test that responses that would have been allowed by CORB after sniffing are
// included in the AllowedByCorbButNotCors UMA.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromProgrammaticContentScript_AllowedAfterSniffing) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a cross-origin fetch to
// cross-site.com (to a PNG image that is incorrectly labelled as
// `Content-Type: text/html`).
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
GURL cross_site_resource(embedded_test_server()->GetURL(
"cross-site.com", "/downloads/image-labeled-as-html.png"));
std::string fetch_result =
FetchViaContentScript(cross_site_resource, active_web_contents());
// Verify that CORB sniffing allowed the response.
VerifyNonCorbElligibleFetchFromContentScript(histograms, console_observer,
fetch_result, "\xEF\xBF\xBDPNG");
}
// Test that responses are blocked by CORB, but have empty response body are not
// reported to LogInitiatorSchemeBypassingDocumentBlocking.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromProgrammaticContentScript_EmptyAndBlocked) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a cross-origin fetch to
// cross-site.com.
base::HistogramTester histograms;
content::WebContentsConsoleObserver console_observer(active_web_contents());
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.empty"));
std::string fetch_result =
FetchViaContentScript(cross_site_resource, active_web_contents());
// Verify whether the fetch worked or not (expectations differ depending on
// various factors - see the body of
// VerifyCorbEligibleFetchFromContentScript).
VerifyCorbEligibleFetchFromContentScript(histograms, console_observer,
fetch_result,
"" /* expected_response_body */);
}
// Test that requests from an extension background page use relaxed CORS
// processing.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromBackgroundPage_NoSniffXml) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// This test covers the default incognito mode (spanning mode) where there is
// only a single background page (i.e. no separate incognito background page).
EXPECT_FALSE(IncognitoInfo::IsSplitMode(extension()));
// Performs a cross-origin fetch from the background page.
{
base::HistogramTester histograms;
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
std::string fetch_result = FetchViaBackgroundPage(cross_site_resource);
// Verify that no blocking occurred.
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
VerifyCorbWasDisabled(histograms);
}
}
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromBackgroundPage_NoSniffXml_OriginAccessList) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
content::WebContents* background_web_contents =
ProcessManager::Get(browser()->profile())
->GetBackgroundHostForExtension(extension()->id())
->host_contents();
// The extension manifest has */cross-site.com in it's permissions array.
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
ASSERT_EQ(background_web_contents->GetBrowserContext()
->GetSharedCorsOriginAccessList()
->GetOriginAccessList()
.CheckAccessState(GetExtensionOrigin(), cross_site_resource),
network::cors::OriginAccessList::AccessState::kAllowed);
// This url is intentionally left out of the `permissions` array in the
// extensions manifest.
GURL cross_site_without_permission_resource("other-without-permission.com");
ASSERT_NE(background_web_contents->GetBrowserContext()
->GetSharedCorsOriginAccessList()
->GetOriginAccessList()
.CheckAccessState(GetExtensionOrigin(),
cross_site_without_permission_resource),
network::cors::OriginAccessList::AccessState::kAllowed);
}
// Test that requests from an extension background page use relaxed CORB
// processing in `no-cors` mode. See also https://crbug.com/1252173.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromBackgroundPage_NoSniffXml_NoCors) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// This test covers the default incognito mode (spanning mode) where there is
// only a single background page (i.e. no separate incognito background page).
EXPECT_FALSE(IncognitoInfo::IsSplitMode(extension()));
// Performs a cross-origin fetch from the background page in "no-cors" mode.
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
base::Value request_init(base::Value::Type::DICTIONARY);
request_init.SetStringPath("mode", "no-cors");
std::string script =
CreateFetchScript(cross_site_resource, std::move(request_init));
content::WebContents* background_web_contents =
ProcessManager::Get(browser()->profile())
->GetBackgroundHostForExtension(extension()->id())
->host_contents();
content::DOMMessageQueue message_queue(background_web_contents);
content::ExecuteScriptAsync(background_web_contents, script);
std::string fetch_result = PopString(&message_queue);
// Verify that no blocking occurred (this is a bit unusual, as "no-cors"
// responses are normally "opaque" - their body is normally not exposed to
// Javascript). See also https://crbug.com/1252173.
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
// Test that requests from an extension background page use relaxed CORB
// processing. This test covers split-mode extensions - see:
// https://developer.chrome.com/docs/extensions/mv2/manifest/incognito/#split)
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromBackgroundPage_IncognitoSplitMode) {
ASSERT_TRUE(embedded_test_server()->Start());
// Install a split-mode extension with permission to cross-site.com.
TestExtensionDir extension_dir;
constexpr char kManifest[] = R"(
{
"name": "Split-mode CORS-testing extension",
"version": "1.0",
"manifest_version": 2,
"incognito": "split",
"permissions": [ "*://cross-site.com/*" ],
"background": {
"scripts": ["bg_script.js"]
}
} )";
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("bg_script.js"), R"(
if (chrome.extension.inIncognitoContext) {
chrome.test.sendMessage('Ready: incognito');
} else {
chrome.test.sendMessage('Ready: not incognito');
} )");
const Extension* extension = nullptr;
{
ExtensionTestMessageListener listener("Ready: not incognito");
extension = LoadExtension(extension_dir.UnpackedPath(),
{.allow_in_incognito = true});
ASSERT_TRUE(extension);
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
// This test covers the split-mode incognito mode where there is a separate
// background page for the regular profile and a separate background page for
// the incognito profile.
EXPECT_TRUE(IncognitoInfo::IsSplitMode(extension));
// Open an incognito window. (The incognito-specific background host for the
// extension will be created after creating a window.)
Browser* incognito_browser = nullptr;
{
ExtensionTestMessageListener listener("Ready: incognito");
incognito_browser = CreateIncognitoBrowser();
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
// Both the regular and the incognito background pages should be able to
// bypass CORS for accessing the `cross_site_resource`.
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
{
SCOPED_TRACE("Regular profile's background page");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
{
SCOPED_TRACE("Incognito profile's background page");
std::string fetch_result = FetchViaBackgroundPage(
cross_site_resource, extension, incognito_browser);
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
}
// Test that CORB+CORS are enforced for extensions with no permissions to
// http/https origins.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
ExtensionWithNoHttpPermissions) {
ASSERT_TRUE(embedded_test_server()->Start());
// "permission" entry in the manifest below mimics the PDF extension which
// has no permissions to http/https origins (and therefore doesn't care
// about relaxed CORB and/or CORS).
const char kManifest[] = R"(
{
"name": "CrossOriginReadBlockingTest - Extension",
"version": "1.0",
"manifest_version": 2,
"permissions": [ "contentSettings" ],
"background": {"scripts": ["background_script.js"]}
} )";
ASSERT_TRUE(InstallExtensionWithManifest(kManifest));
// Perform a cross-origin CORS fetch from the background page.
{
base::HistogramTester histograms;
GURL cross_site_resource1(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.empty"));
std::string fetch_result = FetchViaBackgroundPage(cross_site_resource1);
// Verify that the fetch was blocked by CORS.
//
// This behavior verification is a bit important, but here it mostly tests
// the test setup, rather than the behavior this test was created for.
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
// CORB only applies to `no-cors` requests.
VerifyFetchWasAllowedByCorb(histograms, false /* expecting_sniffing */);
}
// Perform a cross-origin `no-cors` request from the background page.
{
// Use a slightly different URL to avoid having to think what effect the
// network cache might have on test results.
GURL cross_site_resource2(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
base::Value request_init(base::Value::Type::DICTIONARY);
request_init.SetStringPath("method", "GET");
request_init.SetStringPath("mode", "no-cors");
content::WebContents* background_web_contents =
ProcessManager::Get(browser()->profile())
->GetBackgroundHostForExtension(extension()->id())
->host_contents();
base::HistogramTester histograms;
content::DOMMessageQueue queue(background_web_contents);
content::ExecuteScriptAsync(
background_web_contents,
CreateFetchScript(cross_site_resource2, std::move(request_init)));
std::string fetch_result = PopString(&queue);
// Verify that the fetch was blocked by CORB.
//
// This is the main verification in the test. This verifies that the
// extension background page uses a URLLoaderFactory created with
// URLLoaderFactoryParams::is_corb_enabled set to the default, secure value
// of `true`.
EXPECT_EQ("", fetch_result); // `no-cors` = empty, opaque response.
VerifyFetchWasBlockedByCorb(histograms);
}
}
// Test that CORB+CORS are enforced for extensions with optional permissions to
// http/https origins.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
ExtensionWithOptionalHttpPermissions) {
ASSERT_TRUE(embedded_test_server()->Start());
// There are only *optional* HTTP permissions declared in the manifest below -
// they will be granted later, at runtime (but not at the extension
// installation time).
const char kManifest[] = R"(
{
"name": "CrossOriginReadBlockingTest - Extension",
"version": "1.0",
"manifest_version": 2,
"optional_permissions": [ "<all_urls>", "http://example.com/" ],
"background": {"scripts": ["background_script.js"]}
} )";
ASSERT_TRUE(InstallExtensionWithManifest(kManifest));
// Navigate a tab to the extension origin.
GURL extension_resource = GetExtensionResource("page.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), extension_resource));
content::RenderFrameHost* test_frame = browser()
->tab_strip_model()
->GetActiveWebContents()
->GetPrimaryMainFrame();
ASSERT_EQ(GetExtensionOrigin(), test_frame->GetLastCommittedOrigin());
// Perform a cross-origin fetch from an extension frame and verify that it got
// blocked by CORS.
GURL cross_site_url(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
url::Origin cross_site_origin = url::Origin::Create(cross_site_url);
{
base::HistogramTester histograms;
std::string fetch_result = FetchViaFrame(cross_site_url, test_frame);
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
// In presence of optional HTTP permissions CORB is disabled.
VerifyCorbWasDisabled(histograms);
}
// Grant the optional permissions to the extension.
{
PermissionsRequestFunction::SetAutoConfirmForTests(true);
const char kPermissionGrantingScriptTemplate[] = R"(
chrome.permissions.request(
{ origins: [$1] },
granted => { domAutomationController.send(granted); });
)";
bool has_permission_been_granted = false;
std::string script = content::JsReplace(kPermissionGrantingScriptTemplate,
cross_site_origin.GetURL());
ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
test_frame, script, &has_permission_been_granted));
ASSERT_TRUE(has_permission_been_granted);
PermissionsRequestFunction::SetAutoConfirmForTests(false);
}
// Performs a cross-origin fetch from the background page and verify that it
// didn't get blocked by CORS.
{
base::HistogramTester histograms;
std::string fetch_result = FetchViaFrame(cross_site_url, test_frame);
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
// In presence of optional HTTP permissions CORB is disabled.
VerifyCorbWasDisabled(histograms);
}
}
// Test that requests from an extension page hosted in a foreground tab use
// relaxed CORB processing.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromForegroundPage_NoSniffXml) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// This test covers the default incognito mode (spanning mode) where there is
// only a single background page (i.e. no separate incognito background page),
// but multiple processes (one per profile) for extension frames.
EXPECT_FALSE(IncognitoInfo::IsSplitMode(extension()));
// Open an extension frame both in the regular window and in a new incognito
// window.
GURL extension_resource = GetExtensionResource("page.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), extension_resource));
content::WebContents* incognito_contents = nullptr;
{
GURL http_test_page = GetTestPageUrl("fetch-initiator.com");
Browser* incognito_browser =
OpenURLOffTheRecord(browser()->profile(), http_test_page);
incognito_contents =
incognito_browser->tab_strip_model()->GetActiveWebContents();
ASSERT_EQ(http_test_page, incognito_contents->GetLastCommittedURL());
// Open an extension *subframe*. Spanning-mode extensions cannot load in
// main frames of incognito tabs as enforced by ExtensionCanLoadInIncognito
// in //extensions/browser/extension_protocols.cc and described in
// https://developer.chrome.com/docs/extensions/mv2/manifest/incognito/#spanning
const char kScriptTemplate[] = R"(
var iframe = document.createElement('iframe');
iframe.src = $1;
document.body.appendChild(iframe);
)";
{
content::TestNavigationObserver navigation_observer(incognito_contents);
content::ExecuteScriptAsync(
incognito_contents,
content::JsReplace(kScriptTemplate, extension_resource));
navigation_observer.Wait();
ASSERT_TRUE(navigation_observer.last_navigation_succeeded());
ASSERT_EQ(extension_resource, navigation_observer.last_navigation_url());
}
}
content::RenderFrameHost* regular_frame = browser()
->tab_strip_model()
->GetActiveWebContents()
->GetPrimaryMainFrame();
ASSERT_EQ(GetExtensionOrigin(), regular_frame->GetLastCommittedOrigin());
content::RenderFrameHost* incognito_frame =
content::ChildFrameAt(incognito_contents->GetPrimaryMainFrame(), 0);
ASSERT_TRUE(incognito_frame);
ASSERT_EQ(GetExtensionOrigin(), incognito_frame->GetLastCommittedOrigin());
// Test case #1: Fetch from a regular profile's foreground tab.
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
{
SCOPED_TRACE("Regular profile's foreground tab - main frame");
std::string fetch_result =
FetchViaFrame(cross_site_resource, regular_frame);
// Verify that no blocking occurred.
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
// Test case #2: Fetch from an about:srcdoc subframe of a
// chrome-extension://... frame.
{
SCOPED_TRACE("Regular profile's foreground tab - srcdoc frame");
std::string fetch_result =
FetchViaSrcDocFrame(cross_site_resource, regular_frame);
// Verify that no blocking occurred.
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
// Test case #3: Fetch from an extension subframe in an incognito foreground
// tab.
{
SCOPED_TRACE("Incognito profile's foreground tab - subframe");
std::string fetch_result =
FetchViaFrame(cross_site_resource, incognito_frame);
// Verify that no blocking occurred.
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
}
// Test that requests from an extension page hosted in a foreground tab use
// relaxed CORB processing. This test covers split-mode extensions - see:
// https://developer.chrome.com/docs/extensions/mv2/manifest/incognito/#split
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromForegroundPage_IncognitoSplitMode) {
ASSERT_TRUE(embedded_test_server()->Start());
// Install a split-mode extension with permission to cross-site.com.
TestExtensionDir extension_dir;
constexpr char kManifest[] = R"(
{
"name": "Split-mode CORS-testing extension",
"version": "1.0",
"manifest_version": 2,
"incognito": "split",
"permissions": [ "*://cross-site.com/*" ],
"background": { "scripts": ["bg_script.js"] }
} )";
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("bg_script.js"), "");
extension_dir.WriteFile(FILE_PATH_LITERAL("page.html"),
"<body>Hello World!</body>");
const Extension* extension =
LoadExtension(extension_dir.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(extension);
// This test covers the split-mode incognito mode where there is a separate
// background page for the regular profile and a separate background page for
// the incognito profile.
EXPECT_TRUE(IncognitoInfo::IsSplitMode(extension));
// Open an extension tab both in the regular window and in a new incognito
// window.
GURL extension_page = extension->GetResourceURL("page.html");
content::WebContents* regular_contents =
browser()->tab_strip_model()->GetActiveWebContents();
Browser* incognito_browser =
OpenURLOffTheRecord(browser()->profile(), extension_page);
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), extension_page));
content::WebContents* incognito_contents =
incognito_browser->tab_strip_model()->GetActiveWebContents();
ASSERT_EQ(extension->origin(),
regular_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
ASSERT_EQ(
extension->origin(),
incognito_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Test fetching a cross-site resource.
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
{
SCOPED_TRACE("Regular profile's foreground tab");
std::string fetch_result =
FetchViaFrame(cross_site_resource, regular_contents);
// Verify that no blocking occurred.
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
{
SCOPED_TRACE("Incognito profile's foreground tab");
std::string fetch_result =
FetchViaFrame(cross_site_resource, incognito_contents);
// Verify that no blocking occurred.
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
}
// Test that requests from an extension's service worker to the network use
// relaxed CORB processing (both in the case of requests that 1) are initiated
// by the service worker and/or 2) are ignored by the service worker and fall
// back to the network).
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromRegisteredServiceWorker_NoSniffXml) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Register the service worker which injects "SERVICE WORKER INTERCEPT: "
// prefix to the body of each response.
const char kServiceWorkerScript[] = R"(
self.addEventListener('fetch', function(event) {
// Intercept all http requests to cross-site.com and inject
// 'SERVICE WORKER INTERCEPT:' prefix.
if (event.request.url.startsWith('http://cross-site.com')) {
event.respondWith(
// By using the 'fetch' call below, the service worker initiates
// a network request that will go through the URLLoaderFactory
// created via CreateFactoryBundle called / posted indirectly
// from EmbeddedWorkerInstance::StartTask::Start.
fetch(event.request)
.then(response => response.text())
.then(text => new Response(
'SERVICE WORKER INTERCEPT: >>>' + text + '<<<')));
}
// Let the request go directly to the network in all the other cases,
// like:
// - loading the extension resources like page.html (avoiding going
// through the service worker is required for correctness of test
// setup),
// - handling the cross-origin fetch to other.com in test case #2.
// Note that these requests will use the URLLoaderFactory owned by
// ServiceWorkerSubresourceLoader which can be different to the
// network loader factory owned by the ServiceWorker thread (which is
// used for fetch intiated by the service worker above).
}); )";
ASSERT_TRUE(RegisterServiceWorkerForExtension(kServiceWorkerScript));
// Navigate a tab to an extension page.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(),
GetExtensionResource("page.html")));
ASSERT_EQ(
GetExtensionOrigin(),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Verify that the service worker controls the fetches.
bool is_controlled_by_service_worker = false;
ASSERT_TRUE(ExecuteScriptAndExtractBool(
active_web_contents(),
"domAutomationController.send(!!navigator.serviceWorker.controller)",
&is_controlled_by_service_worker));
ASSERT_TRUE(is_controlled_by_service_worker);
// Test case #1: Network fetch initiated by the service worker.
//
// This covers URLLoaderFactory owned by the ServiceWorker thread and created
// created via CreateFactoryBundle called / posted indirectly from
// EmbeddedWorkerInstance::StartTask::Start.
{
// Perform a cross-origin fetch from the foreground extension page.
// This should be intercepted by the service worker installed above.
GURL cross_site_resource_intercepted_by_service_worker(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
std::string fetch_result =
FetchViaFrame(cross_site_resource_intercepted_by_service_worker,
active_web_contents());
// Verify that no blocking occurred (and that the response really did go
// through the service worker).
EXPECT_EQ("SERVICE WORKER INTERCEPT: >>>nosniff.xml - body\n<<<",
fetch_result);
}
// Test case #2: Network fetch used as a fallback when service worker ignores
// the 'fetch' event.
//
// This covers URLLoaderFactory owned by the ServiceWorkerSubresourceLoader,
// which can be different to the network loader factory owned by the
// ServiceWorker thread (which is used in test case #1).
{
// Perform a cross-origin fetch from the foreground extension page.
// This should be intercepted by the service worker installed above.
GURL cross_site_resource_ignored_by_service_worker(
embedded_test_server()->GetURL("other-with-permission.com",
"/nosniff.xml"));
std::string fetch_result = FetchViaFrame(
cross_site_resource_ignored_by_service_worker, active_web_contents());
// Verify that no blocking occurred.
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
}
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_WIN) || \
BUILDFLAG(IS_MAC)
// Flaky on Linux, especially under sanitizers: https://crbug.com/1073052
// Flaky UAF on Mac under ASAN: https://crbug.com/1082355
#define MAYBE_FromBackgroundServiceWorker_NoSniffXml \
DISABLED_FromBackgroundServiceWorker_NoSniffXml
#else
#define MAYBE_FromBackgroundServiceWorker_NoSniffXml \
FromBackgroundServiceWorker_NoSniffXml
#endif
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
MAYBE_FromBackgroundServiceWorker_NoSniffXml) {
// Install the extension with a service worker that can be asked to start a
// fetch to an arbitrary URL.
const char kManifestTemplate[] = R"(
{
"name": "CrossOriginReadBlockingTest - Extension/BgServiceWorker",
"version": "1.0",
"manifest_version": 2,
"permissions": [
"*://cross-site.com/*"
// This list intentionally does NOT include
// other-without-permission.com.
],
"background": {"service_worker": "sw.js"}
} )";
const char kServiceWorker[] = R"(
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.url) {
fetch(request.url)
.then(response => response.text())
.then(text => sendResponse(text))
.catch(err => sendResponse('error: ' + err));
return true;
}
});
)";
dir_.WriteManifest(kManifestTemplate);
dir_.WriteFile(FILE_PATH_LITERAL("sw.js"), kServiceWorker);
dir_.WriteFile(FILE_PATH_LITERAL("page.html"), "<body>Hello World!</body>");
const Extension* extension = LoadExtension(dir_.UnpackedPath());
ASSERT_TRUE(extension);
// Navigate a foreground tab to an extension URL, so that from this tab we can
// ask the background service worker to initiate test fetches.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), extension->GetResourceURL("page.html")));
const char kFetchTemplate[] = R"(
chrome.runtime.sendMessage({url: $1}, function(response) {
domAutomationController.send(response);
});
)";
ASSERT_TRUE(embedded_test_server()->Start());
// Test a request to a website covered by extension permissions.
{
GURL nosniff_xml_with_permission(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
content::DOMMessageQueue queue(active_web_contents());
base::HistogramTester histograms;
content::ExecuteScriptAsync(
active_web_contents(),
content::JsReplace(kFetchTemplate, nosniff_xml_with_permission));
std::string fetch_result = PopString(&queue);
// Verify that no CORB or CORS blocking occurred.
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
// CORB should be disabled for extension origins.
VerifyCorbWasDisabled(histograms);
}
// Test a request to a website *not* covered by extension permissions.
{
GURL nosniff_xml_with_permission(embedded_test_server()->GetURL(
"other-without-permission.com", "/nosniff.xml"));
content::DOMMessageQueue queue(active_web_contents());
base::HistogramTester histograms;
ServiceWorkerConsoleObserver console_observer(
active_web_contents()->GetBrowserContext());
content::ExecuteScriptAsync(
active_web_contents(),
content::JsReplace(kFetchTemplate, nosniff_xml_with_permission));
std::string fetch_result = PopString(&queue);
// Verify that CORS blocked the response.
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
console_observer.WaitForMessages();
VerifyFetchWasBlockedByCors(console_observer);
// CORB should be disabled for extension origins.
VerifyCorbWasDisabled(histograms);
}
}
class ReadyToCommitWaiter : public content::WebContentsObserver {
public:
explicit ReadyToCommitWaiter(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents) {}
ReadyToCommitWaiter(const ReadyToCommitWaiter&) = delete;
ReadyToCommitWaiter& operator=(const ReadyToCommitWaiter&) = delete;
~ReadyToCommitWaiter() override {}
void Wait() { run_loop_.Run(); }
void ReadyToCommitNavigation(
content::NavigationHandle* navigation_handle) override {
run_loop_.Quit();
}
private:
base::RunLoop run_loop_;
};
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
ProgrammaticContentScriptVsWebUI) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Try to inject a content script just as we are about to commit a WebUI page.
// This will cause ExecuteCodeInTabFunction::CanExecuteScriptOnPage to execute
// while RenderFrameHost::GetLastCommittedOrigin() still corresponds to the
// old page.
{
// Initiate navigating a new, blank tab (this avoids process swaps which
// would otherwise occur when navigating to a WebUI pages from either the
// NTP or from a web page). This simulates choosing "Settings" from the
// main menu.
GURL web_ui_url("chrome://settings");
NavigateParams nav_params(
browser(), web_ui_url,
ui::PageTransitionFromInt(ui::PAGE_TRANSITION_GENERATED));
nav_params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
content::WebContentsAddedObserver new_web_contents_observer;
Navigate(&nav_params);
// Capture the new WebContents.
content::WebContents* new_web_contents =
new_web_contents_observer.GetWebContents();
ReadyToCommitWaiter ready_to_commit_waiter(new_web_contents);
content::TestNavigationObserver navigation_observer(new_web_contents, 1);
// Repro of https://crbug.com/894766 requires that no cross-process swap
// takes place - this is what happens when navigating an initial/blank tab.
// Wait until ReadyToCommit happens.
//
// For the repro to happen, content script injection needs to run
// 1) after RenderFrameHostImpl::CommitNavigation
// (which runs in the same revolution of the message pump as
// WebContentsObserver::ReadyToCommitNavigation)
// 2) before RenderFrameHostImpl::DidCommitProvisionalLoad
// (task posted from ReadyToCommitNavigation above should execute
// before any IPC responses that come after PostTask call).
ready_to_commit_waiter.Wait();
DCHECK_NE(
web_ui_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
// Inject the content script (simulating chrome.tabs.executeScript, but
// using TabsExecuteScriptFunction directly to ensure the right timing).
int tab_id = ExtensionTabUtil::GetTabId(active_web_contents());
const char kArgsTemplate[] = R"(
[%d, {"code": "
var p = document.createElement('p');
p.innerText = 'content script injection succeeded unexpectedly';
p.id = 'content-script-injection-result';
document.body.appendChild(p);
"}] )";
std::string args = base::StringPrintf(kArgsTemplate, tab_id);
auto function = base::MakeRefCounted<TabsExecuteScriptFunction>();
function->set_extension(extension());
std::string actual_error =
extension_function_test_utils::RunFunctionAndReturnError(
function.get(), args, browser());
std::string expected_error =
"Cannot access contents of url \"chrome://settings/\". "
"Extension manifest must request permission to access this host.";
EXPECT_EQ(expected_error, actual_error);
// Wait until the navigation completes.
navigation_observer.Wait();
EXPECT_EQ(
web_ui_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
}
// Check if the injection above succeeded (it shouldn't have, because of
// renderer-side checks).
const char kInjectionVerificationScript[] = R"(
domAutomationController.send(
!!document.getElementById('content-script-injection-result')); )";
bool has_content_script_run = false;
EXPECT_TRUE(content::ExecuteScriptAndExtractBool(active_web_contents(),
kInjectionVerificationScript,
&has_content_script_run));
EXPECT_FALSE(has_content_script_run);
// Try to fetch a WebUI resource (i.e. verify that the unsucessful content
// script injection above didn't clobber the WebUI-specific URLLoaderFactory).
const char kScript[] = R"(
var img = document.createElement('img');
img.src = 'chrome://resources/images/arrow_down.svg';
img.onload = () => domAutomationController.send('LOADED');
img.onerror = e => domAutomationController.send('ERROR: ' + e);
)";
std::string result;
EXPECT_TRUE(content::ExecuteScriptAndExtractString(active_web_contents(),
kScript, &result));
EXPECT_EQ("LOADED", result);
}
class CorbAndCorsAppBrowserTest : public PlatformAppBrowserTest {
public:
CorbAndCorsAppBrowserTest() = default;
void SetUpOnMainThread() override {
PlatformAppBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
content::SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
};
IN_PROC_BROWSER_TEST_F(CorbAndCorsAppBrowserTest, WebViewContentScript) {
// Load the test app.
const char kManifest[] = R"(
{
"name": "CrossOriginReadBlockingTest - App",
"version": "1.0",
"manifest_version": 2,
"permissions": ["*://*/*", "webview"],
"app": {
"background": {
"scripts": ["background_script.js"]
}
}
} )";
TestExtensionDir dir;
dir.WriteManifest(kManifest);
const char kBackgroundScript[] = R"(
chrome.app.runtime.onLaunched.addListener(function() {
chrome.app.window.create('page.html', {}, function () {});
});
)";
dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), kBackgroundScript);
const char kPage[] = R"(
<div id="webview-tag-container"></div>
)";
dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPage);
const Extension* app = LoadExtension(dir.UnpackedPath());
ASSERT_TRUE(app);
// Launch the test app and grab its WebContents.
content::WebContents* app_contents = nullptr;
{
content::WebContentsAddedObserver new_contents_observer;
LaunchPlatformApp(app);
app_contents = new_contents_observer.GetWebContents();
}
ASSERT_TRUE(content::WaitForLoadStop(app_contents));
// Inject a <webview> script and declare desire to inject
// cross-origin-fetching content scripts into the guest.
const char kWebViewInjectionScriptTemplate[] = R"(
document.querySelector('#webview-tag-container').innerHTML =
'<webview style="width: 100px; height: 100px;"></webview>';
var webview = document.querySelector('webview');
webview.addContentScripts([{
name: 'rule',
matches: ['*://*/*'],
js: { code: $1 },
run_at: 'document_start'}]);
)";
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml"));
std::string web_view_injection_script = content::JsReplace(
kWebViewInjectionScriptTemplate, CreateFetchScript(cross_site_resource));
ASSERT_TRUE(ExecuteScript(app_contents, web_view_injection_script));
// Navigate <webview>, which should trigger content script execution.
GURL guest_url(
embedded_test_server()->GetURL("fetch-initiator.com", "/title1.html"));
const char kWebViewNavigationScriptTemplate[] = R"(
var webview = document.querySelector('webview');
webview.src = $1;
)";
std::string web_view_navigation_script =
content::JsReplace(kWebViewNavigationScriptTemplate, guest_url);
{
// NOTE: The Dom message will be emitted in a new WebContents instance that
// is created when setting webview.src. Hence, we need to listen for the
// message in that instance.
content::WebContents* webview_contents = nullptr;
{
content::WebContentsAddedObserver webview_contents_added_observer;
content::ExecuteScriptAsync(app_contents, web_view_navigation_script);
webview_contents = webview_contents_added_observer.GetWebContents();
}
content::DOMMessageQueue queue(webview_contents);
std::string fetch_result = PopString(&queue);
// Verify that no CORB or CORS blocking occurred.
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
}
using OriginHeaderExtensionBrowserTest = CorbAndCorsExtensionBrowserTest;
IN_PROC_BROWSER_TEST_F(OriginHeaderExtensionBrowserTest,
OriginHeaderInCrossOriginGetRequest) {
const char kResourcePath[] = "/simulated-resource";
net::test_server::ControllableHttpResponse http_request(
embedded_test_server(), kResourcePath);
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a cross-origin GET fetch to
// cross-site.com.
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", kResourcePath));
const char* kScriptTemplate = R"(
fetch($1, {method: 'GET', mode:'cors'})
.then(response => response.text())
.then(text => domAutomationController.send(text))
.catch(err => domAutomationController.send('ERROR: ' + err));
)";
ExecuteContentScript(
active_web_contents(),
content::JsReplace(kScriptTemplate, cross_site_resource));
// Extract the Origin header.
http_request.WaitForRequest();
std::string actual_origin_header = "<none>";
const auto& headers_map = http_request.http_request()->headers;
auto it = headers_map.find("Origin");
if (it != headers_map.end())
actual_origin_header = it->second;
// Verify the Origin header uses the page's origin (not the extension
// origin).
EXPECT_EQ(url::Origin::Create(page_url).Serialize(), actual_origin_header);
// Regression test against https://crbug.com/944704.
EXPECT_THAT(actual_origin_header,
::testing::Not(::testing::HasSubstr("chrome-extension")));
}
IN_PROC_BROWSER_TEST_F(OriginHeaderExtensionBrowserTest,
OriginHeaderInCrossOriginPostRequest) {
const char kResourcePath[] = "/simulated-resource";
net::test_server::ControllableHttpResponse http_request(
embedded_test_server(), kResourcePath);
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a cross-origin POST fetch to
// cross-site.com.
GURL cross_site_resource(
embedded_test_server()->GetURL("cross-site.com", kResourcePath));
const char* kScriptTemplate = R"(
fetch($1, {method: 'POST', mode:'cors'})
.then(response => response.text())
.then(text => domAutomationController.send(text))
.catch(err => domAutomationController.send('ERROR: ' + err));
)";
ExecuteContentScript(
active_web_contents(),
content::JsReplace(kScriptTemplate, cross_site_resource));
// Extract the Origin header.
http_request.WaitForRequest();
std::string actual_origin_header = "<none>";
const auto& headers_map = http_request.http_request()->headers;
auto it = headers_map.find("Origin");
if (it != headers_map.end())
actual_origin_header = it->second;
// Verify the Origin header uses the page's origin (not the extension
// origin).
EXPECT_EQ(url::Origin::Create(page_url).Serialize(), actual_origin_header);
// Regression test against https://crbug.com/944704.
EXPECT_THAT(actual_origin_header,
::testing::Not(::testing::HasSubstr("chrome-extension")));
}
IN_PROC_BROWSER_TEST_F(OriginHeaderExtensionBrowserTest,
OriginHeaderInSameOriginPostRequest) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to a fetch-initiator.com page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a same-origin POST fetch to
// fetch-initiator.com.
GURL same_origin_resource(
embedded_test_server()->GetURL("fetch-initiator.com", "/echoall"));
const char* kScriptTemplate = R"(
fetch($1, {method: 'POST', mode:'cors'})
.then(response => response.text())
.then(text => domAutomationController.send(text))
.catch(err => domAutomationController.send('ERROR: ' + err));
)";
content::DOMMessageQueue message_queue(active_web_contents());
ExecuteContentScript(
active_web_contents(),
content::JsReplace(kScriptTemplate, same_origin_resource));
std::string fetch_result = PopString(&message_queue);
// Verify the Origin header.
//
// According to the Fetch spec, POST should always set the Origin header (even
// for same-origin requests).
EXPECT_THAT(fetch_result,
::testing::HasSubstr("Origin: http://fetch-initiator.com"));
// Regression test against https://crbug.com/944704.
EXPECT_THAT(fetch_result,
::testing::Not(::testing::HasSubstr("Origin: chrome-extension")));
}
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
RequestHeaders_InSameOriginFetch_FromContentScript) {
// Sec-Fetch-Site only works on secure origins - setting up a https test
// server to help with this.
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.AddDefaultHandlers(GetChromeTestDataDir());
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK);
net::test_server::ControllableHttpResponse subresource_request(
&https_server, "/subresource");
ASSERT_TRUE(https_server.Start());
// Load the test extension.
ASSERT_TRUE(InstallExtension());
// Navigate to https test page.
GURL page_url = https_server.GetURL("/title1.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a same-origin GET fetch.
GURL same_origin_resource(https_server.GetURL("/subresource"));
EXPECT_EQ(url::Origin::Create(page_url),
url::Origin::Create(same_origin_resource));
const char* kScriptTemplate = R"(
fetch($1, {method: 'GET', mode: 'no-cors'}) )";
ExecuteContentScript(
active_web_contents(),
content::JsReplace(kScriptTemplate, same_origin_resource));
// Verify the Referrer and Sec-Fetch-* header values.
subresource_request.WaitForRequest();
EXPECT_THAT(
subresource_request.http_request()->headers,
testing::IsSupersetOf({testing::Pair("Referer", page_url.spec().c_str()),
testing::Pair("Sec-Fetch-Mode", "no-cors"),
testing::Pair("Sec-Fetch-Site", "same-origin")}));
}
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
RequestHeaders_InSameOriginXhr_FromContentScript) {
// Sec-Fetch-Site only works on secure origins - setting up a https test
// server to help with this.
net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
https_server.AddDefaultHandlers(GetChromeTestDataDir());
https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK);
net::test_server::ControllableHttpResponse subresource_request(
&https_server, "/subresource");
ASSERT_TRUE(https_server.Start());
// Load the test extension.
ASSERT_TRUE(InstallExtension());
// Navigate to https test page.
GURL page_url = https_server.GetURL("/title1.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
url::Origin::Create(page_url),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a same-origin GET XHR.
GURL same_origin_resource(https_server.GetURL("/subresource"));
EXPECT_EQ(url::Origin::Create(page_url),
url::Origin::Create(same_origin_resource));
const char* kScriptTemplate = R"(
var req = new XMLHttpRequest();
req.open('GET', $1, true);
req.send(null); )";
ExecuteContentScript(
active_web_contents(),
content::JsReplace(kScriptTemplate, same_origin_resource));
// Verify the Referrer and Sec-Fetch-* header values.
subresource_request.WaitForRequest();
EXPECT_THAT(
subresource_request.http_request()->headers,
testing::IsSupersetOf({testing::Pair("Referer", page_url.spec().c_str()),
testing::Pair("Sec-Fetch-Mode", "cors"),
testing::Pair("Sec-Fetch-Site", "same-origin")}));
}
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest, CorsFromContentScript) {
std::string cors_resource_path = "/cors-subresource-to-intercept";
net::test_server::ControllableHttpResponse cors_request(
embedded_test_server(), cors_resource_path);
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(InstallExtension());
// Navigate to test page.
GURL page_url = GetTestPageUrl("fetch-initiator.com");
url::Origin page_origin = url::Origin::Create(page_url);
std::string page_origin_string = page_origin.Serialize();
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));
ASSERT_EQ(
page_url,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL());
ASSERT_EQ(
page_origin,
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
// Inject a content script that performs a cross-origin GET fetch.
content::DOMMessageQueue message_queue(active_web_contents());
GURL cors_resource_url(
embedded_test_server()->GetURL("cross-site.com", cors_resource_path));
EXPECT_TRUE(ExecuteContentScript(active_web_contents(),
CreateFetchScript(cors_resource_url)));
// Verify the request headers (e.g. Origin and Sec-Fetch-Site headers).
//
// Content scripts should participate in regular CORS, just as if the request
// was issued from the webpage that the content script got injected into.
// Therefore we should expect the Origin header to be present and have the
// right value.
cors_request.WaitForRequest();
EXPECT_THAT(
cors_request.http_request()->headers,
testing::Contains(testing::Pair("Origin", page_origin_string.c_str())));
// Respond with Access-Control-Allow-Origin that matches the origin of the web
// page.
cors_request.Send("HTTP/1.1 200 OK\r\n");
cors_request.Send("Content-Type: text/xml; charset=utf-8\r\n");
cors_request.Send("X-Content-Type-Options: nosniff\r\n");
cors_request.Send("Access-Control-Allow-Origin: " + page_origin_string +
"\r\n");
cors_request.Send("\r\n");
cors_request.Send("cors-allowed-body");
cors_request.Done();
// Verify that no CORB blocking occurred.
//
// CORB blocks responses based on Access-Control-Allow-Origin, oblivious to
// whether the Origin request header was present (and/or if the extension is
// exempted from CORS). The Access-Control-Allow-Origin header is compared
// with the request_initiator of the fetch (the origin of |page_url|) and the
// test responds with "*" which matches all origins.
std::string fetch_result = PopString(&message_queue);
EXPECT_EQ("cors-allowed-body", fetch_result);
}
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromBackgroundPage_ActiveTabPermission) {
ASSERT_TRUE(embedded_test_server()->Start());
TestExtensionDir extension_dir;
constexpr char kManifest[] = R"(
{
"name": "ActiveTab permissions vs CORS from extension background page",
"version": "1.0",
"manifest_version": 2,
"browser_action": {
"default_title": "activeTab"
},
"permissions": ["activeTab"],
"background": {
"scripts": ["bg_script.js"]
}
} )";
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("bg_script.js"), "");
const Extension* extension = LoadExtension(extension_dir.UnpackedPath(),
{.allow_in_incognito = false});
ASSERT_TRUE(extension);
// This test covers the default incognito mode (spanning mode) where there is
// only a single background page (i.e. no separate incognito background page).
EXPECT_FALSE(IncognitoInfo::IsSplitMode(extension));
// Set up a test scenario:
// - top-level frame: kActiveTabHost
constexpr char kActiveTabHost[] = "active-tab.example";
GURL original_document_url =
embedded_test_server()->GetURL(kActiveTabHost, "/title1.html");
GURL cross_site_resource(
embedded_test_server()->GetURL(kActiveTabHost, "/nosniff.xml"));
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), original_document_url));
// Open an incognito window. Since the extension is not enabled for
// incognito, OriginAccessList should not be sent to the incognito-related
// NetworkContext (this is verified by a DCHECK in
// SetCorsOriginAccessListForExtensionHelper in
// //extensions/browser/extension_util.cc.
CreateIncognitoBrowser();
// CORS exception shouldn't be initially granted based on ActiveTab.
{
SCOPED_TRACE("TEST STEP 1: Initial fetch.");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
// Do one pass of BrowserAction without granting ActiveTab permission,
// extension still shouldn't have ability to bypass CORS.
ExtensionActionRunner::GetForWebContents(active_web_contents())
->RunAction(extension, false);
{
SCOPED_TRACE("TEST STEP 2: After BrowserAction without granting access.");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
// Granting ActiveTab permission to the extension should give it the ability
// to bypass CORS.
ExtensionActionRunner::GetForWebContents(active_web_contents())
->RunAction(extension, true);
{
// ActiveTab access (just like OOR-CORS access) extends to the background
// page. This is desirable, because
// 1) there is no security boundary between A) extension background pages
// and B) extension frames in the tab
// 2) it seems best to highlight #1 by simplistically granting extra
// capabilities to the whole extension (rather than forcing the extension
// authors to jump through extra hurdles to utilize the new capability).
SCOPED_TRACE("TEST STEP 3: After granting ActiveTab access.");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
// Navigating the tab to a different, same-origin document should retain
// extension's access to the origin.
GURL another_document_url =
embedded_test_server()->GetURL(kActiveTabHost, "/title2.html");
EXPECT_NE(another_document_url, original_document_url);
EXPECT_EQ(url::Origin::Create(another_document_url),
url::Origin::Create(original_document_url));
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), another_document_url));
{
SCOPED_TRACE(
"TEST STEP 4: After navigating the tab cross-document, "
"but still same-origin.");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
// Navigating the tab to a different origin should revoke extension's access
// to the tab.
GURL cross_origin_url =
embedded_test_server()->GetURL("other.com", "/title1.html");
EXPECT_NE(url::Origin::Create(cross_origin_url),
url::Origin::Create(original_document_url));
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), cross_origin_url));
{
SCOPED_TRACE("TEST STEP 5: After navigating the tab cross-origin.");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
}
// Similar to FromBackgroundPage_ActiveTabPermission, but focues on interaction
// between the regular background page and the separate incognito background
// page in "split" mode.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromBackgroundPage_ActiveTabPermission_SplitMode) {
TestExtensionDir extension_dir;
constexpr char kManifest[] = R"(
{
"name": "ActiveTab permissions vs CORS from extension background page",
"version": "1.0",
"manifest_version": 2,
"browser_action": {
"default_title": "activeTab"
},
"incognito": "split",
"permissions": ["activeTab"],
"background": {
"scripts": ["bg_script.js"]
}
} )";
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("bg_script.js"), "");
const Extension* extension =
LoadExtension(extension_dir.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(extension);
// Set up a test scenario:
// - regular window: empty initial tab
// - incognito window: top-level frame: kActiveTabHost
ASSERT_TRUE(embedded_test_server()->Start());
constexpr char kActiveTabHost[] = "active-tab.example";
GURL original_document_url =
embedded_test_server()->GetURL(kActiveTabHost, "/title1.html");
Browser* incognito_browser =
OpenURLOffTheRecord(browser()->profile(), original_document_url);
// CORS exception shouldn't be initially granted based on ActiveTab.
GURL cross_site_resource(
embedded_test_server()->GetURL(kActiveTabHost, "/nosniff.xml"));
{
SCOPED_TRACE("TEST STEP 1: Initial fetch.");
{
SCOPED_TRACE("Regular profile's background page");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
{
SCOPED_TRACE("Incognito profile's background page");
std::string fetch_result = FetchViaBackgroundPage(
cross_site_resource, extension, incognito_browser);
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
}
// Granting ActiveTab permission in the incognito window should give the
// extension access to the tab's origin, but only in the incognito profile
// (since the extension uses "split" mode).
ExtensionActionRunner::GetForWebContents(
incognito_browser->tab_strip_model()->GetActiveWebContents())
->RunAction(extension, true);
{
SCOPED_TRACE("TEST STEP 2: After granting ActiveTab access.");
{
SCOPED_TRACE("Regular profile's background page");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
{
SCOPED_TRACE("Incognito profile's background page");
std::string fetch_result = FetchViaBackgroundPage(
cross_site_resource, extension, incognito_browser);
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
}
// Navigating the tab to a different origin should revoke extension's access
// to the tab.
GURL cross_origin_url =
embedded_test_server()->GetURL("other.com", "/title1.html");
EXPECT_NE(url::Origin::Create(cross_origin_url),
url::Origin::Create(original_document_url));
ASSERT_TRUE(
ui_test_utils::NavigateToURL(incognito_browser, cross_origin_url));
{
SCOPED_TRACE("TEST STEP 3: After navigating the tab cross-origin.");
{
SCOPED_TRACE("Regular profile's background page");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
{
SCOPED_TRACE("Incognito profile's background page");
std::string fetch_result = FetchViaBackgroundPage(
cross_site_resource, extension, incognito_browser);
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
}
}
// Similar to FromBackgroundPage_ActiveTabPermission_SplitMode, but goes through
// steps that (at one point) forced additional, persistent leaking of incognito
// permission into the regular profile's background page. See also
// https://crbug.com/1167262.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromBackgroundPage_ActiveTabPermission_SplitMode2) {
TestExtensionDir extension_dir;
constexpr char kManifest[] = R"(
{
"name": "ActiveTab permissions vs CORS from extension background page",
"version": "1.0",
"manifest_version": 2,
"browser_action": {
"default_title": "activeTab"
},
"incognito": "split",
"permissions": ["activeTab"],
"background": {
"scripts": ["bg_script.js"]
}
} )";
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("bg_script.js"), "");
const Extension* extension =
LoadExtension(extension_dir.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(extension);
// Set up a test scenario:
// - regular window: top-level frame: kRegularHost
// - incognito window: top-level frame: kIncognitoHost
ASSERT_TRUE(embedded_test_server()->Start());
constexpr char kIncognitoHost[] = "active-tab-in-incognito-profile.example";
constexpr char kRegularHost[] = "active-tab-in-regular-profile.example";
GURL incognito_page_url =
embedded_test_server()->GetURL(kIncognitoHost, "/title1.html");
GURL incognito_resource_url =
embedded_test_server()->GetURL(kIncognitoHost, "/nosniff.xml");
GURL regular_page_url =
embedded_test_server()->GetURL(kRegularHost, "/title2.html");
GURL regular_resource_url =
embedded_test_server()->GetURL(kRegularHost, "/nosniff.xml");
Browser* incognito_browser =
OpenURLOffTheRecord(browser()->profile(), incognito_page_url);
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), regular_page_url));
// No CORS exception for `kIncognitoHost` should be initially granted based on
// ActiveTab.
{
SCOPED_TRACE("TEST STEP 1: Initial fetch.");
{
SCOPED_TRACE("Regular profile's background page");
std::string fetch_result =
FetchViaBackgroundPage(incognito_resource_url, extension, browser());
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
{
SCOPED_TRACE("Incognito profile's background page");
std::string fetch_result = FetchViaBackgroundPage(
incognito_resource_url, extension, incognito_browser);
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
}
// Granting ActiveTab permission in the *incognito* window should give the
// extension access to the tab's origin, but only in the incognito profile
// (since the extension uses "split" mode).
ExtensionActionRunner::GetForWebContents(
incognito_browser->tab_strip_model()->GetActiveWebContents())
->RunAction(extension, true);
{
SCOPED_TRACE("TEST STEP 2: After granting 'incognito' ActiveTab access.");
{
SCOPED_TRACE("Regular profile's background page");
std::string fetch_result =
FetchViaBackgroundPage(incognito_resource_url, extension, browser());
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
}
{
SCOPED_TRACE("Incognito profile's background page");
std::string fetch_result = FetchViaBackgroundPage(
incognito_resource_url, extension, incognito_browser);
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
}
// Granting ActiveTab permission in the *regular* window (for a separate,
// `kRegularHost`) should not affect how CORS behaved in the previous step
// (unless there is a bug and we leak incognito permissions to the regular
// background page).
content::WebContents* regular_contents =
browser()->tab_strip_model()->GetActiveWebContents();
EXPECT_EQ(
kRegularHost,
regular_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().host());
EXPECT_NE(kRegularHost, kIncognitoHost);
ExtensionActionRunner::GetForWebContents(regular_contents)
->RunAction(extension, true);
{
SCOPED_TRACE("TEST STEP 3: After granting 'regular' ActiveTab access.");
{
SCOPED_TRACE("Regular profile's background page");
std::string fetch_result =
FetchViaBackgroundPage(incognito_resource_url, extension, browser());
// TODO(https://crbug.com/1167262): Change to EXPECT_EQ after fixing the
// leak of permissions from incognito profile to regular profile.
EXPECT_NE(kCorsErrorWhenFetching, fetch_result);
}
{
SCOPED_TRACE("Incognito profile's background page");
std::string fetch_result = FetchViaBackgroundPage(
incognito_resource_url, extension, incognito_browser);
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
}
}
// After closing the incognito window, the regular background page should
// still have no access to the `kIncognitoHost` (or, hopefully, the potential
// leaks of permissions from the previous steps should be fixed/recovered-from
// at this point).
incognito_browser->tab_strip_model()->GetActiveWebContents()->Close();
{
SCOPED_TRACE("TEST STEP 4: After closing the incognito tab.");
{
SCOPED_TRACE("Regular profile's background page");
std::string fetch_result =
FetchViaBackgroundPage(incognito_resource_url, extension, browser());
// TODO(https://crbug.com/1167262): Change to EXPECT_EQ after fixing the
// leak of permissions from incognito profile to regular profile.
EXPECT_NE(kCorsErrorWhenFetching, fetch_result);
}
}
}
// Similar to FromBackgroundPage_ActiveTabPermission, but focues on behavior
// of the background page when it is shared between the regular and the
// incognito profiles in "spanning" mode.
IN_PROC_BROWSER_TEST_F(CorbAndCorsExtensionBrowserTest,
FromBackgroundPage_ActiveTabPermission_SpanningMode) {
TestExtensionDir extension_dir;
constexpr char kManifest[] = R"(
{
"name": "ActiveTab permissions vs CORS from extension background page",
"version": "1.0",
"manifest_version": 2,
"browser_action": {
"default_title": "activeTab"
},
"incognito": "spanning",
"permissions": ["activeTab"],
"background": {
"scripts": ["bg_script.js"]
}
} )";
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("bg_script.js"), "");
const Extension* extension =
LoadExtension(extension_dir.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(extension);
// Set up a test scenario:
// - regular window: empty initial tab
// - incognito window: top-level frame: kActiveTabHost
ASSERT_TRUE(embedded_test_server()->Start());
constexpr char kActiveTabHost[] = "active-tab.example";
GURL original_document_url =
embedded_test_server()->GetURL(kActiveTabHost, "/title1.html");
Browser* incognito_browser =
OpenURLOffTheRecord(browser()->profile(), original_document_url);
// CORS exception shouldn't be initially granted based on ActiveTab.
GURL cross_site_resource(
embedded_test_server()->GetURL(kActiveTabHost, "/nosniff.xml"));
{
SCOPED_TRACE("TEST STEP 1: Initial fetch.");
SCOPED_TRACE("Regular profile's background page");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
// There is no separate incognito background page in "spanning" mode.
}
// Granting ActiveTab permission in the incognito window should give the
// extension access to the tab's origin.
ExtensionActionRunner::GetForWebContents(
incognito_browser->tab_strip_model()->GetActiveWebContents())
->RunAction(extension, true);
{
SCOPED_TRACE("TEST STEP 2: After granting ActiveTab access.");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ("nosniff.xml - body\n", fetch_result);
// There is no separate incognito background page in "spanning" mode.
}
// Navigating the tab to a different origin should revoke extension's access
// to the tab.
GURL cross_origin_url =
embedded_test_server()->GetURL("other.com", "/title1.html");
EXPECT_NE(url::Origin::Create(cross_origin_url),
url::Origin::Create(original_document_url));
ASSERT_TRUE(
ui_test_utils::NavigateToURL(incognito_browser, cross_origin_url));
{
SCOPED_TRACE("TEST STEP 3: After navigating the tab cross-origin.");
std::string fetch_result =
FetchViaBackgroundPage(cross_site_resource, extension, browser());
EXPECT_EQ(kCorsErrorWhenFetching, fetch_result);
// There is no separate incognito background page in "spanning" mode.
}
}
} // namespace extensions