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