| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/files/file_path.h" |
| #include "base/json/json_reader.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/bind_test_util.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/apps/app_service/app_launch_params.h" |
| #include "chrome/browser/apps/app_service/app_service_proxy.h" |
| #include "chrome/browser/apps/app_service/app_service_proxy_factory.h" |
| #include "chrome/browser/apps/app_service/browser_app_launcher.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_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/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/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/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/url_loader_factory_manager.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/cross_origin_read_blocking.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 "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| enum TestParam { |
| // Whether the extension under test is "allowlisted" (see |
| // GetExtensionsAllowlist in |
| // //extensions/browser/url_loader_factory_manager.cc). |
| kAllowlisted = 1 << 0, |
| |
| // Whether network::features::kOutOfBlinkCors is enabled. |
| kOutOfBlinkCors = 1 << 1, |
| |
| // Whether network::features::kCorbAllowlistAlsoAppliesToOorCors is enabled. |
| kAllowlistForCors = 1 << 2, |
| |
| // Whether network::features:: |
| // kDeriveOriginFromUrlForNeitherGetNorHeadRequestWhenHavingSpecialAccess is |
| // enabled. |
| kDeriveOriginFromUrl = 1 << 3, |
| }; |
| |
| const char kCorsErrorWhenFetching[] = "error: TypeError: Failed to fetch"; |
| |
| // The manifest.json used by tests uses |kExpectedKey| that will result in the |
| // hash of extension id that is captured in |kExpectedHashedExtensionId|. |
| // Knowing the hash constant helps with simulating distributing the hash via |
| // field trial param (e.g. via CorbAllowlistAlsoAppliesToOorCorsParamName). |
| const char kExtensionKey[] = |
| "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjzv7dI7Ygyh67VHE1DdidudpYf8PFf" |
| "v8iucWvzO+3xpF/" |
| "Dm5xNo7aQhPNiEaNfHwJQ7lsp4gc+C+4bbaVewBFspTruoSJhZc5uEfqxwovJwN+v1/" |
| "SUFXTXQmQBv6gs0qZB4gBbl4caNQBlqrFwAMNisnu1V6UROna8rOJQ90D7Nv7TCwoVPKBfVshp" |
| "FjdDOTeBg4iLctO3S/" |
| "06QYqaTDrwVceSyHkVkvzBY6tc6mnYX0RZu78J9iL8bdqwfllOhs69cqoHHgrLdI6JdOyiuh6p" |
| "BP6vxMlzSKWJ3YTNjaQTPwfOYaLMuzdl0v+YdzafIzV9zwe4Xiskk+5JNGt8b2rQIDAQAB"; |
| const char kExpectedHashedExtensionId[] = |
| "14B587526D9AC6ADCACAA8A4AAE3DB281CA2AB53"; |
| |
| // 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="; |
| |
| } // namespace |
| |
| using CORBAction = network::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()); |
| } |
| |
| std::string CreateFetchScript( |
| const GURL& resource, |
| base::Optional<base::Value> request_init = base::nullopt) { |
| CHECK(request_init == base::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)); |
| base::Optional<base::Value> value = |
| base::JSONReader::Read(json, base::JSON_ALLOW_TRAILING_COMMAS); |
| std::string result; |
| EXPECT_TRUE(value->GetAsString(&result)); |
| return result; |
| } |
| |
| protected: |
| TestExtensionDir dir_; |
| }; |
| |
| class ServiceWorkerConsoleObserver |
| : public content::ServiceWorkerContextObserver { |
| public: |
| explicit ServiceWorkerConsoleObserver( |
| content::BrowserContext* browser_context) |
| : scoped_observer_(this) { |
| content::StoragePartition* partition = |
| content::BrowserContext::GetDefaultStoragePartition(browser_context); |
| scoped_observer_.Add(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_; |
| ScopedObserver<content::ServiceWorkerContext, |
| content::ServiceWorkerContextObserver> |
| scoped_observer_; |
| }; |
| |
| class CorbAndCorsExtensionBrowserTest |
| : public CorbAndCorsExtensionTestBase, |
| public ::testing::WithParamInterface<TestParam> { |
| public: |
| using Base = CorbAndCorsExtensionTestBase; |
| |
| CorbAndCorsExtensionBrowserTest() { |
| std::vector<base::Feature> disabled_features; |
| std::vector<base::test::ScopedFeatureList::FeatureAndParams> |
| enabled_features; |
| |
| if (IsOutOfBlinkCorsEnabled()) { |
| enabled_features.emplace_back(network::features::kOutOfBlinkCors, |
| base::FieldTrialParams()); |
| } else { |
| disabled_features.push_back(network::features::kOutOfBlinkCors); |
| } |
| |
| if (DeriveOriginFromUrl()) { |
| enabled_features.emplace_back( |
| network::features:: |
| kDeriveOriginFromUrlForNeitherGetNorHeadRequestWhenHavingSpecialAccess, |
| base::FieldTrialParams()); |
| } else { |
| disabled_features.push_back( |
| network::features:: |
| kDeriveOriginFromUrlForNeitherGetNorHeadRequestWhenHavingSpecialAccess); |
| } |
| |
| if (ShouldAllowlistAlsoApplyToOorCors()) { |
| base::FieldTrialParams field_trial_params; |
| if (IsExtensionAllowlisted()) { |
| field_trial_params.emplace( |
| network::features::kCorbAllowlistAlsoAppliesToOorCorsParamName, |
| kExpectedHashedExtensionId); |
| } |
| enabled_features.emplace_back( |
| network::features::kCorbAllowlistAlsoAppliesToOorCors, |
| field_trial_params); |
| } else { |
| disabled_features.push_back( |
| network::features::kCorbAllowlistAlsoAppliesToOorCors); |
| } |
| |
| scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features, |
| disabled_features); |
| } |
| |
| void SetUpInProcessBrowserTestFixture() override { |
| EXPECT_CALL(policy_provider_, IsInitializationComplete(testing::_)) |
| .WillRepeatedly(testing::Return(true)); |
| policy_provider_.SetAutoRefresh(); |
| policy::BrowserPolicyConnector::SetPolicyProviderForTesting( |
| &policy_provider_); |
| } |
| |
| bool IsExtensionAllowlisted() { |
| return (GetParam() & TestParam::kAllowlisted) != 0; |
| } |
| |
| bool IsOutOfBlinkCorsEnabled() { |
| return (GetParam() & TestParam::kOutOfBlinkCors) != 0; |
| } |
| |
| // This returns true if content scripts are not exempt from CORS. |
| bool ShouldAllowlistAlsoApplyToOorCors() { |
| return (GetParam() & TestParam::kAllowlistForCors) != 0; |
| } |
| |
| bool DeriveOriginFromUrl() { |
| return (GetParam() & TestParam::kDeriveOriginFromUrl) != 0; |
| } |
| |
| 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"] |
| }], |
| )"; |
| |
| // Note that the hardcoded "key" below matches kExpectedHashedExtensionId |
| // (used by the test suite for allowlisting the extension as needed). |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "CrossOriginReadBlockingTest - Extension", |
| "key": "%s", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": [ |
| "tabs", |
| "*://fetch-initiator.com/*", |
| "*://127.0.0.1/*", // Initiator in AppCache tests. |
| "*://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"]} |
| } )"; |
| dir_.WriteManifest(base::StringPrintf( |
| kManifestTemplate, kExtensionKey, |
| 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()); |
| DCHECK(extension_); |
| |
| AllowlistExtensionIfNeeded(*extension_); |
| return extension_; |
| } |
| |
| bool AreContentScriptFetchesExpectedToBeBlocked() { |
| return !IsExtensionAllowlisted(); |
| } |
| |
| bool IsCorbExpectedToBeTurnedOffAltogether() { |
| return IsExtensionAllowlisted(); |
| } |
| |
| void VerifyPassiveUmaForAllowlistForCors( |
| const base::HistogramTester& histograms, |
| base::Optional<bool> expected_value) { |
| metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting(); |
| |
| const char* kUmaName = |
| "SiteIsolation.XSD.Browser.AllowedByCorbButNotCors.ContentScript"; |
| bool expect_uma_presence = expected_value.has_value(); |
| |
| // This logging is to get an initial estimate, and it won't work once we |
| // actually turn the new CORS content script behavior on. |
| if (IsOutOfBlinkCorsEnabled() && ShouldAllowlistAlsoApplyToOorCors()) |
| expect_uma_presence = false; |
| |
| // If the extension is allowlisted, then CORB is disabled (and therefore the |
| // UMA logging code in CrossOriginReadBlocking::ResponseAnalyzer won't run |
| // at all for allowlisted extensions). |
| if (IsExtensionAllowlisted()) |
| expect_uma_presence = false; |
| |
| // Verify |expect_uma_presence| and |expected_value|. |
| if (!expect_uma_presence) { |
| histograms.ExpectTotalCount(kUmaName, 0); |
| } else { |
| histograms.ExpectUniqueSample(kUmaName, static_cast<int>(*expected_value), |
| 1); |
| } |
| } |
| |
| // 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 VerifyFetchFromContentScriptWasBlockedByCorb( |
| const base::HistogramTester& histograms) { |
| // Make sure that histograms logged in other processes (e.g. in |
| // NetworkService process) get synced. |
| metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting(); |
| |
| if (IsCorbExpectedToBeTurnedOffAltogether()) { |
| EXPECT_EQ(0u, |
| histograms.GetTotalCountsForPrefix("SiteIsolation.XSD.Browser") |
| .size()); |
| return; |
| } |
| |
| histograms.ExpectBucketCount("SiteIsolation.XSD.Browser.Action", |
| CORBAction::kResponseStarted, 1); |
| histograms.ExpectBucketCount("SiteIsolation.XSD.Browser.Action", |
| CORBAction::kBlockedWithoutSniffing, 1); |
| |
| // If CORB blocks the response, then there is no risk in enabling |
| // CorbAllowlistAlsoAppliesToOorCors and we shouldn't log the UMA. |
| VerifyPassiveUmaForAllowlistForCors(histograms, base::nullopt); |
| } |
| |
| void VerifyFetchFromContentScriptWasAllowedByCorb( |
| 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(); |
| |
| if (IsCorbExpectedToBeTurnedOffAltogether()) { |
| EXPECT_EQ(0u, |
| histograms.GetTotalCountsForPrefix("SiteIsolation.XSD.Browser") |
| .size()); |
| return; |
| } |
| |
| histograms.ExpectBucketCount("SiteIsolation.XSD.Browser.Action", |
| CORBAction::kResponseStarted, 1); |
| histograms.ExpectBucketCount("SiteIsolation.XSD.Browser.Action", |
| expecting_sniffing |
| ? CORBAction::kAllowedAfterSniffing |
| : CORBAction::kAllowedWithoutSniffing, |
| 1); |
| } |
| |
| // Verifies results of fetching a CORB-eligible resource from a content |
| // script. Expectations differ depending on the following: |
| // 1. Allowlisted extension: Fetches from content scripts |
| // should not be blocked |
| // 2. Other extension: Fetches from content scripts should be blocked by |
| // either: only CORB or CORS+CORB. |
| // |
| // 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(); |
| |
| // VerifyCorbEligibleFetchFromContentScript is only called for Content Types |
| // covered by CORB and therefore these requests carry no risk for |
| // CorbAllowlistAlsoAppliesToOorCors - verify that we didn't log the UMA. |
| VerifyPassiveUmaForAllowlistForCors(histograms, base::nullopt); |
| |
| if (AreContentScriptFetchesExpectedToBeBlocked()) { |
| if (ShouldAllowlistAlsoApplyToOorCors()) { |
| // 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). |
| } else { |
| // Verify the fetch was blocked by CORB, but not blocked by CORS. |
| EXPECT_EQ(std::string(), actual_fetch_result); |
| VerifyFetchFromContentScriptWasBlockedByCorb(histograms); |
| } |
| } else { |
| // Verify the fetch was allowed. |
| EXPECT_EQ(expected_fetch_result, actual_fetch_result); |
| VerifyFetchFromContentScriptWasAllowedByCorb(histograms); |
| } |
| } |
| |
| 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 sniffing allowed the response. |
| VerifyFetchFromContentScriptWasAllowedByCorb(histograms, |
| true /* expecting_sniffing */); |
| |
| if (ShouldAllowlistAlsoApplyToOorCors() && |
| AreContentScriptFetchesExpectedToBeBlocked()) { |
| // Verify that the response body was blocked by CORS. |
| EXPECT_EQ(kCorsErrorWhenFetching, actual_fetch_result); |
| VerifyFetchWasBlockedByCors(console_observer); |
| } else { |
| // Verify that the response body was not blocked by either CORB nor CORS. |
| EXPECT_THAT(actual_fetch_result, |
| ::testing::StartsWith(expected_fetch_result_prefix)); |
| } |
| |
| // This is the kind of response (i.e., cross-origin fetch of a non-CORB |
| // type) that could be affected by the planned |
| // CorbAllowlistAlsoAppliesToOorCors feature. |
| VerifyPassiveUmaForAllowlistForCors(histograms, true); |
| } |
| |
| content::WebContents* active_web_contents() { |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| const Extension* InstallExtensionWithPermissionToAllUrls() { |
| // Note that the hardcoded "key" below matches kExpectedHashedExtensionId |
| // (used by the test suite for allowlisting the extension as needed). |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "CrossOriginReadBlockingTest - Extension/AllUrls", |
| "key": "%s", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": [ "tabs", "<all_urls>" ], |
| "background": {"scripts": ["background_script.js"]} |
| } )"; |
| dir_.WriteManifest(base::StringPrintf(kManifestTemplate, kExtensionKey)); |
| dir_.WriteFile(FILE_PATH_LITERAL("background_script.js"), ""); |
| extension_ = LoadExtension(dir_.UnpackedPath()); |
| DCHECK(extension_); |
| |
| AllowlistExtensionIfNeeded(*extension_); |
| return extension_; |
| } |
| |
| bool RegisterServiceWorkerForExtension( |
| const std::string& service_worker_script) { |
| const char kServiceWorkerPath[] = "service_worker.js"; |
| dir_.WriteFile(base::FilePath::FromUTF8Unsafe(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, |
| 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) { |
| return FetchHelper( |
| url, base::BindOnce( |
| &browsertest_util::ExecuteScriptInBackgroundPageNoWait, |
| base::Unretained(browser()->profile()), extension_->id())); |
| } |
| |
| // Performs a fetch of |url| from |web_contents| (directly, without going |
| // through content scripts). Returns the body of the response. |
| std::string FetchViaWebContents(const GURL& url, |
| content::WebContents* web_contents) { |
| return FetchHelper( |
| url, |
| base::BindOnce(&CorbAndCorsExtensionBrowserTest::ExecuteRegularScript, |
| base::Unretained(this), base::Unretained(web_contents))); |
| } |
| |
| // 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, |
| 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 url::Origin::Create(extension_->url()); |
| } |
| |
| 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); |
| } |
| |
| void AllowlistExtensionIfNeeded(const Extension& extension) { |
| // Sanity check that the field trial param (which has to be registered via |
| // ScopedFeatureList early) uses the right extension id hash. |
| EXPECT_EQ(kExpectedHashedExtensionId, extension.hashed_id().value()); |
| |
| if (ShouldAllowlistAlsoApplyToOorCors()) { |
| // Allowlist has already been populated via field trial param (see the |
| // constructor of CrossOriginReadBlockingExtensionAllowlistingTest). |
| return; |
| } |
| |
| // If field trial param cannot be used, fall back to allowlisting via |
| // URLLoaderFactoryManager's test support methods. |
| if (IsExtensionAllowlisted()) { |
| URLLoaderFactoryManager::AddExtensionToAllowlistForTesting(extension); |
| } else { |
| URLLoaderFactoryManager::RemoveExtensionFromAllowlistForTesting( |
| extension); |
| } |
| } |
| |
| protected: |
| 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::WebContents* web_contents, |
| const std::string& regular_script) { |
| content::ExecuteScriptAsync(web_contents, 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, FetchCallback fetch_callback) { |
| content::DOMMessageQueue message_queue; |
| |
| // 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); |
| } |
| |
| const Extension* extension_ = nullptr; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CorbAndCorsExtensionBrowserTest); |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(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; |
| |
| // 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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| EXPECT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| EXPECT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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; |
| |
| // 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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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; |
| 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 work if the extension is allowlisted. |
| // |
| // 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; |
| 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; |
| content::ExecuteScriptAsync(active_web_contents(), kFetchInitiatingScript); |
| std::string fetch_result = PopString(&queue); |
| |
| if (IsExtensionAllowlisted() && IsOutOfBlinkCorsEnabled()) { |
| // TODO(lukasza): https://crbug.com/1062043: Revoking of extension |
| // permissions doesn't cover |
| // URLLoaderFactoryParams::factory_bound_access_patterns. |
| EXPECT_EQ("nosniff.xml - body\n", fetch_result); |
| VerifyFetchFromContentScriptWasAllowedByCorb(histograms); |
| } else { |
| EXPECT_EQ(kCorsErrorWhenFetching, fetch_result); |
| VerifyFetchFromContentScriptWasBlockedByCorb(histograms); |
| VerifyFetchWasBlockedByCors(console_observer); |
| } |
| } |
| } |
| |
| // Test that <all_urls> permission does not apply to hosts blocked by policy. |
| IN_PROC_BROWSER_TEST_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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. |
| EXPECT_EQ(kCorsErrorWhenFetching, fetch_result); |
| VerifyFetchFromContentScriptWasBlockedByCorb(histograms); |
| VerifyFetchWasBlockedByCors(console_observer); |
| } |
| } |
| |
| // Test that <all_urls> permission does not apply to hosts blocked by policy. |
| IN_PROC_BROWSER_TEST_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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); |
| VerifyFetchFromContentScriptWasAllowedByCorb(histograms, |
| true /* expecting_sniffing */); |
| VerifyFetchWasBlockedByCors(console_observer); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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 allowlisted |
| // extensions with <all_urls> permission). See also |
| // https://crbug.com/1049604#c14. |
| IN_PROC_BROWSER_TEST_P( |
| 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 passing kFlagEnableFileAccess in |
| // ExtensionBrowserTest::LoadExtension). |
| const Extension* extension = InstallExtensionWithPermissionToAllUrls(); |
| 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. |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->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; |
| 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; |
| 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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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); |
| VerifyFetchFromContentScriptWasAllowedByCorb(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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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 the response sniffed as |
| // didn't sniff as html/xml/json). |
| VerifyFetchFromContentScriptWasAllowedByCorb(histograms, |
| true /* 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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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")); |
| VerifyFetchFromContentScriptWasAllowedByCorb(histograms, |
| false /* expecting_sniffing */); |
| |
| // Same-origin requests are not at risk of being broken. |
| VerifyPassiveUmaForAllowlistForCors(histograms, false); |
| } |
| |
| // Test that responses that would have been allowed by CORB anyway are not |
| // reported to LogInitiatorSchemeBypassingDocumentBlocking. |
| IN_PROC_BROWSER_TEST_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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 Feature 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::kTrustTokens); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| INSTANTIATE_TEST_SUITE_P(Allowlisted_AllowlistForCors, |
| TrustTokenExtensionBrowserTest, |
| ::testing::Values(TestParam::kAllowlisted | |
| TestParam::kOutOfBlinkCors | |
| TestParam::kAllowlistForCors)); |
| IN_PROC_BROWSER_TEST_P( |
| TrustTokenExtensionBrowserTest, |
| 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"); |
| 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; |
| |
| base::Value request_init(base::Value::Type::DICTIONARY); |
| request_init.SetStringPath("trustToken.type", "srr-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() |
| ->GetMainFrame() |
| ->FlushNetworkAndNavigationInterfacesForTesting(); |
| { |
| content::DOMMessageQueue message_queue; |
| |
| base::Value request_init(base::Value::Type::DICTIONARY); |
| request_init.SetStringPath("trustToken.type", "srr-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_P( |
| 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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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 LogInitiatorSchemeBypassingDocumentBlocking exits early for |
| // requests that aren't from content scripts. |
| IN_PROC_BROWSER_TEST_P(CorbAndCorsExtensionBrowserTest, |
| FromBackgroundPage_NoSniffXml) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| ASSERT_TRUE(InstallExtension()); |
| |
| // 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); |
| } |
| |
| // Test that requests from a extension page hosted in a foreground tab use |
| // relaxed CORB processing. |
| IN_PROC_BROWSER_TEST_P(CorbAndCorsExtensionBrowserTest, |
| FromForegroundPage_NoSniffXml) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| ASSERT_TRUE(InstallExtension()); |
| |
| // Navigate a tab to an extension page. |
| ui_test_utils::NavigateToURL(browser(), GetExtensionResource("page.html")); |
| ASSERT_EQ(GetExtensionOrigin(), |
| active_web_contents()->GetMainFrame()->GetLastCommittedOrigin()); |
| |
| // Test case #1: Fetch from a chrome-extension://... main frame. |
| { |
| // Perform a cross-origin fetch from the foreground extension page. |
| base::HistogramTester histograms; |
| GURL cross_site_resource( |
| embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml")); |
| std::string fetch_result = |
| FetchViaWebContents(cross_site_resource, active_web_contents()); |
| |
| // 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. |
| { |
| // Perform a cross-origin fetch from the foreground extension page. |
| base::HistogramTester histograms; |
| GURL cross_site_resource( |
| embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml")); |
| std::string fetch_result = FetchViaSrcDocFrame( |
| cross_site_resource, active_web_contents()->GetMainFrame()); |
| |
| // 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_P(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. |
| ui_test_utils::NavigateToURL(browser(), GetExtensionResource("page.html")); |
| ASSERT_EQ(GetExtensionOrigin(), |
| active_web_contents()->GetMainFrame()->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. |
| base::HistogramTester histograms; |
| GURL cross_site_resource_intercepted_by_service_worker( |
| embedded_test_server()->GetURL("cross-site.com", "/nosniff.xml")); |
| std::string fetch_result = |
| FetchViaWebContents(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. |
| base::HistogramTester histograms; |
| GURL cross_site_resource_ignored_by_service_worker( |
| embedded_test_server()->GetURL("other-with-permission.com", |
| "/nosniff.xml")); |
| std::string fetch_result = FetchViaWebContents( |
| cross_site_resource_ignored_by_service_worker, active_web_contents()); |
| |
| // Verify that no blocking occurred. |
| EXPECT_EQ("nosniff.xml - body\n", fetch_result); |
| } |
| } |
| |
| #if defined(OS_LINUX) || defined(OS_CHROMEOS) || defined(OS_WIN) || \ |
| defined(OS_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_P(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", |
| "key": "%s", |
| "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(base::StringPrintf(kManifestTemplate, kExtensionKey)); |
| 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); |
| AllowlistExtensionIfNeeded(*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. |
| 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; |
| 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. |
| metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting(); |
| EXPECT_EQ( |
| 0u, |
| histograms.GetTotalCountsForPrefix("SiteIsolation.XSD.Browser").size()); |
| } |
| |
| // 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; |
| 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. |
| metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting(); |
| EXPECT_EQ( |
| 0u, |
| histograms.GetTotalCountsForPrefix("SiteIsolation.XSD.Browser").size()); |
| } |
| } |
| |
| class ReadyToCommitWaiter : public content::WebContentsObserver { |
| public: |
| explicit ReadyToCommitWaiter(content::WebContents* web_contents) |
| : content::WebContentsObserver(web_contents) {} |
| |
| ~ReadyToCommitWaiter() override {} |
| |
| void Wait() { run_loop_.Run(); } |
| |
| void ReadyToCommitNavigation( |
| content::NavigationHandle* navigation_handle) override { |
| run_loop_.Quit(); |
| } |
| |
| private: |
| base::RunLoop run_loop_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ReadyToCommitWaiter); |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(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()->GetMainFrame()->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()->GetMainFrame()->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); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(CorbAndCorsExtensionBrowserTest, |
| ProgrammaticContentScriptVsAppCache) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| ASSERT_TRUE(InstallExtension()); |
| |
| // Set up http server serving files from content/test/data (which conveniently |
| // already contains appcache-related test files, unlike chrome/test/data). |
| std::string origin = "http://127.0.0.1:8080"; |
| std::unique_ptr<content::URLLoaderInterceptor> url_loader_interceptor = |
| content::URLLoaderInterceptor::ServeFilesFromDirectoryAtOrigin( |
| "content/test/data", GURL(origin)); |
| |
| // Load the main page twice. The second navigation should have AppCache |
| // initialized for the page. |
| // |
| // Note that localhost / 127.0.0.1 need to be used, because Application Cache |
| // is restricted to secure contexts. |
| GURL main_url(origin + "/appcache/simple_page_with_manifest.html"); |
| ui_test_utils::NavigateToURL(browser(), main_url); |
| base::string16 expected_title = base::ASCIIToUTF16("AppCache updated"); |
| content::TitleWatcher title_watcher(active_web_contents(), expected_title); |
| EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); |
| ui_test_utils::NavigateToURL(browser(), main_url); |
| |
| // Turn off the server and sanity check that the resource is still available. |
| const char kScriptTemplate[] = R"( |
| new Promise(function (resolve, reject) { |
| var img = document.createElement('img'); |
| img.src = '/appcache/' + $1; |
| img.onload = _ => resolve('IMG LOADED'); |
| img.onerror = reject; |
| }) |
| )"; |
| EXPECT_EQ("IMG LOADED", |
| content::EvalJs(active_web_contents(), |
| content::JsReplace(kScriptTemplate, "logo.png"))); |
| |
| // Inject a content script and verify that this doesn't negatively impact |
| // AppCache (i.e. verify that |
| // RenderFrameHostImpl::MarkInitiatorsAsRequiringSeparateURLLoaderFactory |
| // does not clobber the default URLLoaderFactory). |
| { |
| metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting(); |
| 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"); |
| } |
| // Using a different image, to bypass renderer-side caching. |
| EXPECT_EQ("IMG LOADED", |
| content::EvalJs(active_web_contents(), |
| content::JsReplace(kScriptTemplate, "logo2.png"))); |
| |
| // Crash the network service and wait for things to come back up. This (and |
| // the remaining part of the test) only makes sense if 1) the network service |
| // is enabled and running in a separate process and 2) the frame has at least |
| // one network-bound URLLoaderFactory (i.e. the test extension is |
| // allowlisted). |
| if (!content::IsOutOfProcessNetworkService() || !IsExtensionAllowlisted()) |
| return; |
| SimulateNetworkServiceCrash(); |
| active_web_contents() |
| ->GetMainFrame() |
| ->FlushNetworkAndNavigationInterfacesForTesting(); |
| |
| // Make sure that both requests still work - the code should have recovered |
| // from the crash by 1) refreshing the URLLoaderFactory for the content script |
| // and 2) without cloberring the default factory for the AppCache. |
| { |
| metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting(); |
| 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"); |
| } |
| // Using a different image, to bypass renderer-side caching. |
| EXPECT_EQ("IMG LOADED", |
| content::EvalJs(active_web_contents(), |
| content::JsReplace(kScriptTemplate, "logo3.png"))); |
| } |
| |
| using CorbAndCorsAppBrowserTest = CorbAndCorsExtensionTestBase; |
| |
| IN_PROC_BROWSER_TEST_F(CorbAndCorsAppBrowserTest, WebViewContentScript) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // 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"] |
| } |
| } |
| } )"; |
| dir_.WriteManifest(kManifest); |
| const char kBackgroungScript[] = R"( |
| chrome.app.runtime.onLaunched.addListener(function() { |
| chrome.app.window.create('page.html', {}, function () {}); |
| }); |
| )"; |
| dir_.WriteFile(FILE_PATH_LITERAL("background_script.js"), kBackgroungScript); |
| 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; |
| apps::AppServiceProxyFactory::GetForProfile(browser()->profile()) |
| ->BrowserAppLauncher() |
| ->LaunchAppWithParams(apps::AppLaunchParams( |
| app->id(), LaunchContainer::kLaunchContainerNone, |
| WindowOpenDisposition::NEW_WINDOW, |
| apps::mojom::AppLaunchSource::kSourceTest)); |
| 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); |
| { |
| content::DOMMessageQueue queue; |
| base::HistogramTester histograms; |
| content::ExecuteScriptAsync(app_contents, web_view_navigation_script); |
| std::string fetch_result = PopString(&queue); |
| |
| // Verify that no CORB blocking occurred. |
| EXPECT_EQ("nosniff.xml - body\n", fetch_result); |
| } |
| } |
| |
| using OriginHeaderExtensionBrowserTest = CorbAndCorsExtensionBrowserTest; |
| |
| IN_PROC_BROWSER_TEST_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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; |
| |
| if (AreContentScriptFetchesExpectedToBeBlocked() && |
| ShouldAllowlistAlsoApplyToOorCors()) { |
| // Verify the Origin header uses the page's origin (not the extension |
| // origin). |
| EXPECT_EQ(url::Origin::Create(page_url).Serialize(), actual_origin_header); |
| } else { |
| // Verify the Origin header is missing. |
| EXPECT_EQ("<none>", 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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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; |
| 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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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_P(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"); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(url::Origin::Create(page_url), |
| active_web_contents()->GetMainFrame()->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_P(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(); |
| ui_test_utils::NavigateToURL(browser(), page_url); |
| ASSERT_EQ(page_url, |
| active_web_contents()->GetMainFrame()->GetLastCommittedURL()); |
| ASSERT_EQ(page_origin, |
| active_web_contents()->GetMainFrame()->GetLastCommittedOrigin()); |
| |
| // Inject a content script that performs a cross-origin GET fetch. |
| content::DOMMessageQueue message_queue; |
| 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). |
| cors_request.WaitForRequest(); |
| if (IsExtensionAllowlisted() || !ShouldAllowlistAlsoApplyToOorCors()) { |
| // Content scripts of allowlisted extensions should be exempted from CORS, |
| // based on the websites the extension has permission for, via extension |
| // manifest. Therefore, there should be no "Origin" header. |
| EXPECT_THAT( |
| cors_request.http_request()->headers, |
| testing::Not(testing::Contains(testing::Pair("Origin", testing::_)))); |
| } else { |
| // Content scripts of non-allowlisted extensions 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. |
| 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); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(Allowlisted_AllowlistForCors, |
| CorbAndCorsExtensionBrowserTest, |
| ::testing::Values(TestParam::kAllowlisted | |
| TestParam::kOutOfBlinkCors | |
| TestParam::kAllowlistForCors)); |
| INSTANTIATE_TEST_SUITE_P(NotAllowlisted_AllowlistForCors, |
| CorbAndCorsExtensionBrowserTest, |
| ::testing::Values(TestParam::kOutOfBlinkCors | |
| TestParam::kAllowlistForCors)); |
| INSTANTIATE_TEST_SUITE_P(Allowlisted_OorCors, |
| CorbAndCorsExtensionBrowserTest, |
| ::testing::Values(TestParam::kAllowlisted | |
| TestParam::kOutOfBlinkCors)); |
| INSTANTIATE_TEST_SUITE_P(NotAllowlisted_OorCors, |
| CorbAndCorsExtensionBrowserTest, |
| ::testing::Values(TestParam::kOutOfBlinkCors)); |
| INSTANTIATE_TEST_SUITE_P(Allowlisted_InBlinkCors, |
| CorbAndCorsExtensionBrowserTest, |
| ::testing::Values(TestParam::kAllowlisted)); |
| INSTANTIATE_TEST_SUITE_P(NotAllowlisted_InBlinkCors, |
| CorbAndCorsExtensionBrowserTest, |
| ::testing::Values(0)); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| Allowlisted_LegacyOriginHeaderBehavior_AllowlistForCors, |
| OriginHeaderExtensionBrowserTest, |
| ::testing::Values(TestParam::kAllowlisted | TestParam::kAllowlistForCors | |
| TestParam::kOutOfBlinkCors)); |
| INSTANTIATE_TEST_SUITE_P(Allowlisted_NewOriginHeaderBehavior_AllowlistForCors, |
| OriginHeaderExtensionBrowserTest, |
| ::testing::Values(TestParam::kAllowlisted | |
| TestParam::kAllowlistForCors | |
| TestParam::kOutOfBlinkCors | |
| TestParam::kDeriveOriginFromUrl)); |
| INSTANTIATE_TEST_SUITE_P( |
| NotAllowlisted_LegacyOriginHeaderBehavior_AllowlistForCors, |
| OriginHeaderExtensionBrowserTest, |
| ::testing::Values(TestParam::kOutOfBlinkCors | |
| TestParam::kAllowlistForCors)); |
| INSTANTIATE_TEST_SUITE_P( |
| NotAllowlisted_NewOriginHeaderBehavior_AllowlistForCors, |
| OriginHeaderExtensionBrowserTest, |
| ::testing::Values(TestParam::kOutOfBlinkCors | |
| TestParam::kAllowlistForCors | |
| TestParam::kDeriveOriginFromUrl)); |
| INSTANTIATE_TEST_SUITE_P(Allowlisted_LegacyOriginHeaderBehavior, |
| OriginHeaderExtensionBrowserTest, |
| ::testing::Values(TestParam::kAllowlisted | |
| TestParam::kOutOfBlinkCors)); |
| INSTANTIATE_TEST_SUITE_P(Allowlisted_NewOriginHeaderBehavior, |
| OriginHeaderExtensionBrowserTest, |
| ::testing::Values(TestParam::kAllowlisted | |
| TestParam::kOutOfBlinkCors | |
| TestParam::kDeriveOriginFromUrl)); |
| INSTANTIATE_TEST_SUITE_P(NotAllowlisted_LegacyOriginHeaderBehavior, |
| OriginHeaderExtensionBrowserTest, |
| ::testing::Values(TestParam::kOutOfBlinkCors)); |
| INSTANTIATE_TEST_SUITE_P(NotAllowlisted_NewOriginHeaderBehavior, |
| OriginHeaderExtensionBrowserTest, |
| ::testing::Values(TestParam::kOutOfBlinkCors | |
| TestParam::kDeriveOriginFromUrl)); |
| |
| } // namespace extensions |