| // Copyright 2021 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/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "chrome/browser/apps/platform_apps/app_browsertest_util.h" |
| #include "chrome/browser/extensions/extension_browsertest.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/version_info/channel.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/back_forward_cache_util.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/commit_message_delayer.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/test_utils.h" |
| #include "extensions/browser/browsertest_util.h" |
| #include "extensions/browser/content_script_tracker.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/process_manager.h" |
| #include "extensions/browser/script_executor.h" |
| #include "extensions/browser/user_script_manager.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/features/feature_channel.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "extensions/test/test_content_script_load_waiter.h" |
| #include "extensions/test/test_extension_dir.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| namespace extensions { |
| |
| // Asks the |extension_id| to inject |content_script| into |web_contents|. |
| void ExecuteProgrammaticContentScriptNoWait(content::WebContents* web_contents, |
| const ExtensionId& extension_id, |
| const std::string& content_script, |
| const char* message) { |
| // Build a script that executes the original `content_script` and then sends |
| // an ack via `domAutomationController.send`. |
| const char kAckingScriptTemplate[] = R"( |
| %s; |
| domAutomationController.send("%s"); |
| )"; |
| std::string acking_script = base::StringPrintf( |
| kAckingScriptTemplate, content_script.c_str(), message); |
| |
| // Build a script to execute in the extension's background page. |
| int tab_id = ExtensionTabUtil::GetTabId(web_contents); |
| std::string background_script = content::JsReplace( |
| "chrome.tabs.executeScript($1, { code: $2 });", tab_id, acking_script); |
| |
| // Inject the script and wait for the ack. |
| // |
| // Note that using ExtensionTestMessageListener / `chrome.test.sendMessage` |
| // (instead of DOMMessageQueue / `domAutomationController.send`) would have |
| // hung in the ProgrammaticInjectionRacingWithDidCommit testcase. The root |
| // cause is not 100% understood, but it might be because the IPC related to |
| // `chrome.test.sendMessage` can't be dispatched while running a nested |
| // message loop while handling a DidCommit IPC. |
| ASSERT_TRUE(browsertest_util::ExecuteScriptInBackgroundPageNoWait( |
| web_contents->GetBrowserContext(), extension_id, background_script)); |
| } |
| |
| // Asks the |extension_id| to inject |content_script| into |web_contents| and |
| // waits until the script reports that it has finished executing. |
| void ExecuteProgrammaticContentScript(content::WebContents* web_contents, |
| const ExtensionId& extension_id, |
| const std::string& content_script) { |
| content::DOMMessageQueue message_queue(web_contents); |
| ExecuteProgrammaticContentScriptNoWait( |
| web_contents, extension_id, content_script, "Hello from acking script!"); |
| std::string msg; |
| EXPECT_TRUE(message_queue.WaitForMessage(&msg)); |
| EXPECT_EQ("\"Hello from acking script!\"", msg); |
| } |
| |
| // Executes a `script` as a user script associated with the given `extension_id` |
| // within the primary main frame of `web_contents`, waiting for the injection to |
| // complete. |
| void ExecuteUserScript(content::WebContents& web_contents, |
| const ExtensionId& extension_id, |
| const std::string& script) { |
| base::RunLoop run_loop; |
| |
| ScriptExecutor script_executor(&web_contents); |
| std::vector<mojom::JSSourcePtr> sources; |
| sources.push_back(mojom::JSSource::New(script, GURL())); |
| script_executor.ExecuteScript( |
| mojom::HostID(mojom::HostID::HostType::kExtensions, extension_id), |
| mojom::CodeInjection::NewJs(mojom::JSInjection::New( |
| std::move(sources), mojom::ExecutionWorld::kUserScript, |
| blink::mojom::WantResultOption::kWantResult, |
| blink::mojom::UserActivationOption::kDoNotActivate, |
| blink::mojom::PromiseResultOption::kAwait)), |
| ScriptExecutor::SPECIFIED_FRAMES, {ExtensionApiFrameIdMap::kTopFrameId}, |
| ScriptExecutor::DONT_MATCH_ABOUT_BLANK, mojom::RunLocation::kDocumentIdle, |
| ScriptExecutor::DEFAULT_PROCESS, GURL() /* webview_src */, |
| base::IgnoreArgs<std::vector<ScriptExecutor::FrameResult>>( |
| run_loop.QuitWhenIdleClosure())); |
| |
| run_loop.Run(); |
| } |
| |
| // Test suite covering `extensions::ContentScriptTracker` from |
| // //extensions/browser/content_script_tracker.h. |
| // |
| // See also ContentScriptMatchingBrowserTest in |
| // //extensions/browser/content_script_matching_browsertest.cc. |
| class ContentScriptTrackerBrowserTest : public ExtensionBrowserTest { |
| public: |
| ContentScriptTrackerBrowserTest() = default; |
| |
| void SetUpOnMainThread() override { |
| ExtensionBrowserTest::SetUpOnMainThread(); |
| |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| content::SetupCrossSiteRedirector(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| }; |
| |
| // Helper class for executing a content script right before handling a DidCommit |
| // IPC. |
| class ContentScriptExecuterBeforeDidCommit { |
| public: |
| ContentScriptExecuterBeforeDidCommit(const GURL& postponed_commit_url, |
| content::WebContents* web_contents, |
| const ExtensionId& extension_id, |
| const std::string& content_script) |
| : message_queue_(web_contents), |
| commit_delayer_( |
| web_contents, |
| postponed_commit_url, |
| base::BindOnce( |
| &ContentScriptExecuterBeforeDidCommit::ExecuteContentScript, |
| base::Unretained(web_contents), |
| extension_id, |
| content_script)) {} |
| |
| void WaitForMessage() { |
| std::string msg; |
| EXPECT_TRUE(message_queue_.WaitForMessage(&msg)); |
| EXPECT_EQ("\"Hello from acking script!\"", msg); |
| } |
| |
| private: |
| static void ExecuteContentScript(content::WebContents* web_contents, |
| const ExtensionId& extension_id, |
| const std::string& content_script, |
| content::RenderFrameHost* ignored) { |
| ExecuteProgrammaticContentScriptNoWait(web_contents, extension_id, |
| content_script, |
| "Hello from acking script!"); |
| } |
| |
| content::DOMMessageQueue message_queue_; |
| content::CommitMessageDelayer commit_delayer_; |
| }; |
| |
| // Tests tracking of content scripts injected/declared via |
| // `chrome.scripting.executeScript` API. See also: |
| // https://developer.chrome.com/docs/extensions/mv3/content_scripts/#programmatic |
| IN_PROC_BROWSER_TEST_F(ContentScriptTrackerBrowserTest, |
| ProgrammaticContentScript) { |
| // Install a test extension. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - Programmatic", |
| "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"), ""); |
| const Extension* extension = LoadExtension(dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Navigate to an arbitrary, mostly-empty test page. |
| GURL page_url = embedded_test_server()->GetURL("foo.com", "/title1.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url)); |
| |
| // Verify that initially no processes show up as having been injected with |
| // content scripts. |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| content::RenderFrameHost* background_frame = |
| ProcessManager::Get(browser()->profile()) |
| ->GetBackgroundHostForExtension(extension->id()) |
| ->main_frame_host(); |
| EXPECT_EQ("This page has no title.", |
| content::EvalJs(web_contents, "document.body.innerText")); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *web_contents->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *background_frame->GetProcess(), extension->id())); |
| |
| // Programmatically inject a content script. |
| const char kContentScript[] = R"( |
| document.body.innerText = 'content script has run'; |
| )"; |
| ExecuteProgrammaticContentScript(web_contents, extension->id(), |
| kContentScript); |
| |
| // Verify that the right processes show up as having been injected with |
| // content scripts. |
| EXPECT_EQ("content script has run", |
| content::EvalJs(web_contents, "document.body.innerText")); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *web_contents->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| // Sanity check: injecting a content script should not count as injecting a |
| // user script. |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunUserScriptFromExtension( |
| *web_contents->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| // And the extension page should never be considered as a content script |
| // target. |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *background_frame->GetProcess(), extension->id())); |
| |
| // Navigate to a different same-site document and verify if |
| // ContentScriptTracker still thinks that content scripts have been injected. |
| // |
| // DidProcessRunContentScriptFromExtension is expected to return true, because |
| // content scripts have been injected into the renderer process in the *past*, |
| // even though the *current* set of documents hosted in the renderer process |
| // have not run a content script. |
| GURL new_url = embedded_test_server()->GetURL("foo.com", "/title2.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), new_url)); |
| EXPECT_EQ("This page has a title.", |
| content::EvalJs(web_contents, "document.body.innerText")); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *web_contents->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *background_frame->GetProcess(), extension->id())); |
| } |
| |
| // Tests tracking of user scripts through the ScriptExecutor. |
| // The vast majority of implementation is the same for content script and user |
| // script tracking, so this is the main spot we explicitly test user script |
| // specific tracking. |
| IN_PROC_BROWSER_TEST_F(ContentScriptTrackerBrowserTest, |
| ProgrammaticUserScript) { |
| // Install a test extension. |
| // TODO(https://crbug.com/1429408): There's currently no way for extensions |
| // to trigger user script injections, so this extension is really just to |
| // have one we force to be associated with the injection. When the userScripts |
| // API is fully developed, we should update this to use the developer-facing |
| // API. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - Programmatic", |
| "version": "1.0", |
| "manifest_version": 3, |
| "host_permissions": ["<all_urls>"] |
| } )"; |
| dir.WriteManifest(kManifestTemplate); |
| const Extension* extension = LoadExtension(dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Navigate to an arbitrary, mostly-empty test page. |
| GURL page_url = embedded_test_server()->GetURL("foo.com", "/title1.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url)); |
| |
| // Verify that initially no processes show up as having been injected with |
| // user scripts. |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_EQ("This page has no title.", |
| content::EvalJs(web_contents, "document.body.innerText")); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunUserScriptFromExtension( |
| *web_contents->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| |
| // Programmatically inject a user script. |
| static constexpr char kUserScript[] = |
| "document.body.innerText = 'user script has run';"; |
| ExecuteUserScript(*web_contents, extension->id(), kUserScript); |
| |
| // Verify that the right processes show up as having been injected with |
| // content scripts. |
| EXPECT_EQ("user script has run", |
| content::EvalJs(web_contents, "document.body.innerText")); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunUserScriptFromExtension( |
| *web_contents->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| // Sanity check: injecting a user script should not count as injecting a |
| // content script. |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *web_contents->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| |
| // Navigate to a different same-site document and verify if |
| // ContentScriptTracker still thinks that user scripts have been injected. |
| // |
| // DidProcessRunUserScriptFromExtension is expected to return true, because |
| // user scripts have been injected into the renderer process in the *past*, |
| // even though the *current* set of documents hosted in the renderer process |
| // have not run a user script. |
| GURL new_url = embedded_test_server()->GetURL("foo.com", "/title2.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), new_url)); |
| EXPECT_EQ("This page has a title.", |
| content::EvalJs(web_contents, "document.body.innerText")); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunUserScriptFromExtension( |
| *web_contents->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| } |
| |
| // Tests what happens when the ExtensionMsg_ExecuteCode is sent *after* sending |
| // a Commit IPC to the renderer (i.e. after ReadyToCommit) but *before* a |
| // corresponding DidCommit IPC has been received by the browser process. See |
| // also the "DocumentUserData race w/ Commit IPC" section in the |
| // document here: |
| // https://docs.google.com/document/d/1MFprp2ss2r9RNamJ7Jxva1bvRZvec3rzGceDGoJ6vW0/edit#heading=h.n2ppjzx4jpzt |
| // TODO(crbug.com/936696): Remove the test after RenderDocument is shipped. |
| IN_PROC_BROWSER_TEST_F(ContentScriptTrackerBrowserTest, |
| ProgrammaticInjectionRacingWithDidCommit) { |
| // The test assumes the RenderFrame stays the same after navigation. Disable |
| // back/forward cache to ensure that RenderFrame swap won't happen. |
| content::DisableBackForwardCacheForTesting( |
| browser()->tab_strip_model()->GetActiveWebContents(), |
| content::BackForwardCache::TEST_ASSUMES_NO_RENDER_FRAME_CHANGE); |
| // Install a test extension. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - DidCommit race", |
| "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"), ""); |
| const Extension* extension = LoadExtension(dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Navigate to an arbitrary, mostly-empty test page. |
| GURL page_url = embedded_test_server()->GetURL("foo.com", "/title1.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url)); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Programmatically inject a content script between ReadyToCommit and |
| // DidCommit events. |
| { |
| GURL new_url = embedded_test_server()->GetURL("foo.com", "/title2.html"); |
| ContentScriptExecuterBeforeDidCommit content_script_executer( |
| new_url, web_contents, extension->id(), |
| "document.body.innerText = 'content script has run'"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), new_url)); |
| content_script_executer.WaitForMessage(); |
| } |
| |
| // Verify that the process shows up as having been injected with content |
| // scripts. |
| EXPECT_EQ("content script has run", |
| content::EvalJs(web_contents, "document.body.innerText")); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *web_contents->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| } |
| |
| // Tests tracking of content scripts injected/declared via `content_scripts` |
| // entry in the extension manifest. See also: |
| // https://developer.chrome.com/docs/extensions/mv3/content_scripts/#static-declarative |
| IN_PROC_BROWSER_TEST_F(ContentScriptTrackerBrowserTest, |
| ContentScriptDeclarationInExtensionManifest) { |
| // Install a test extension. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - Declarative", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": [ "tabs", "<all_urls>" ], |
| "content_scripts": [{ |
| "all_frames": true, |
| "matches": ["*://bar.com/*"], |
| "js": ["content_script.js"] |
| }] |
| } )"; |
| dir.WriteManifest(kManifestTemplate); |
| dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), R"( |
| document.body.innerText = 'content script has run'; |
| chrome.test.sendMessage('Hello from content script!'); |
| )"); |
| const Extension* extension = LoadExtension(dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Navigate to a test page that is *not* covered by `content_scripts.matches` |
| // manifest entry above. |
| GURL ignored_url = embedded_test_server()->GetURL("foo.com", "/title1.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), ignored_url)); |
| content::WebContents* first_tab = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Verify that initially no processes show up as having been injected with |
| // content scripts. |
| EXPECT_EQ("This page has no title.", |
| content::EvalJs(first_tab, "document.body.innerText")); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *first_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| |
| // Navigate to a test page that *is* covered by `content_scripts.matches` |
| // manifest entry above. |
| { |
| GURL injected_url = |
| embedded_test_server()->GetURL("bar.com", "/title1.html"); |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), injected_url, WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| content::WebContents* second_tab = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_NE(first_tab, second_tab); |
| |
| // Verify that content script has been injected. |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| EXPECT_EQ("content script has run", |
| content::EvalJs(second_tab, "document.body.innerText")); |
| |
| // Verify that ContentScriptTracker detected the injection. |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *second_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| } |
| |
| // Verify that the initial tab still is still correctly absent from |
| // ContentScriptTracker. |
| EXPECT_EQ("This page has no title.", |
| content::EvalJs(first_tab, "document.body.innerText")); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *first_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| } |
| |
| // Ensure ContentScriptTracker correctly tracks script injections in frames |
| // which undergo non-network (i.e. no ReadyToCommitNavigation notification) |
| // navigations after an extension is loaded. For more details about the |
| // particular race condition covered by this test please see |
| // https://docs.google.com/document/d/1Z0-C3Bstva_-NK_bKhcyj4f2kdWjXv8pscuHre7UlSk/edit?usp=sharing |
| IN_PROC_BROWSER_TEST_F( |
| ContentScriptTrackerBrowserTest, |
| AboutBlankNavigationAfterLoadingExtensionMidwayThroughTest) { |
| // Navigate to a test page that *is* covered by `content_scripts.matches` |
| // manifest entry below (the extension is *not* installed at this point yet). |
| GURL injected_url = |
| embedded_test_server()->GetURL("example.com", "/title1.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), injected_url)); |
| content::WebContents* first_tab = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Create the test extension. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - Declarative", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": [ "tabs", "<all_urls>" ], |
| "content_scripts": [{ |
| "all_frames": true, |
| "match_about_blank": true, |
| "matches": ["*://example.com/*"], |
| "js": ["content_script.js"], |
| "run_at": "document_end" |
| }], |
| "background": {"scripts": ["background_script.js"]} |
| } )"; |
| dir.WriteManifest(kManifestTemplate); |
| dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), ""); |
| dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), R"( |
| document.body.innerText = 'content script has run'; |
| chrome.test.sendMessage('Hello from content script!'); |
| )"); |
| |
| // Load the test extension. Note that the LoadExtension call below will |
| // internally wait for content scripts to be sent to the renderer processes |
| // (see ContentScriptLoadWaiter usage in the WaitForExtensionReady method). |
| const Extension* extension = LoadExtension(dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| std::string extension_id = extension->id(); |
| |
| // Open a new tab with 'about:blank'. This may be tricky, because 1) the |
| // initial empty document commits synchronously, without going through |
| // ReadyToCommit step and 2) when this test was being written, the initial |
| // 'about:blank' did not send a DidCommit IPC to the Browser process. |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| content::WebContentsAddedObserver popup_observer; |
| ExecuteScriptAsync(first_tab, "window.open('about:blank', '_blank')"); |
| |
| // Verify that the content script has been run. |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| content::WebContents* popup = popup_observer.GetWebContents(); |
| EXPECT_EQ("content script has run", |
| content::EvalJs(popup, "document.body.innerText")); |
| |
| // Verify that content script didn't run in the opener. This mostly verifies |
| // the test setup/steps. |
| EXPECT_NE("content script has run", |
| content::EvalJs(first_tab, "document.body.innerText")); |
| |
| // Verify that ContentScriptTracker correctly says that a content script has |
| // been run in the `popup`. This verifies product code - this is the main |
| // verification in this test. |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *popup->GetPrimaryMainFrame()->GetProcess(), extension_id)); |
| } |
| |
| class ContentScriptTrackerMatchOriginAsFallbackBrowserTest |
| : public ContentScriptTrackerBrowserTest { |
| public: |
| ContentScriptTrackerMatchOriginAsFallbackBrowserTest() { |
| scoped_feature_list_.InitAndEnableFeature( |
| extensions_features::kContentScriptsMatchOriginAsFallback); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Covers detecting content script injection into a 'data:...' URL. |
| IN_PROC_BROWSER_TEST_F( |
| ContentScriptTrackerMatchOriginAsFallbackBrowserTest, |
| ContentScriptDeclarationInExtensionManifest_DataUrlIframe) { |
| // Install a test extension. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - Declarative", |
| "version": "1.0", |
| "manifest_version": 3, |
| "content_scripts": [{ |
| "all_frames": true, |
| "match_about_blank": true, |
| "match_origin_as_fallback": true, |
| "matches": ["*://bar.com/*"], |
| "js": ["content_script.js"] |
| }] |
| } )"; |
| dir.WriteManifest(kManifestTemplate); |
| dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), R"( |
| document.body.innerText = 'content script has run'; |
| chrome.test.sendMessage('Hello from content script!'); )"); |
| const Extension* extension = LoadExtension(dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Navigate to a test page that *is* covered by `content_scripts.matches` |
| // manifest entry above. |
| content::WebContents* first_tab = nullptr; |
| { |
| GURL injected_url = |
| embedded_test_server()->GetURL("bar.com", "/title1.html"); |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), injected_url)); |
| |
| // Verify that content script has been injected. |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| first_tab = browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_EQ("content script has run", |
| content::EvalJs(first_tab, "document.body.innerText")); |
| |
| // Verify that ContentScriptTracker detected the injection. |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *first_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| } |
| |
| // Add a new subframe with a `data:...` URL. This will verify that the |
| // browser-side ContentScriptTracker correctly accounts for the renderer-side |
| // support for injecting contents scripts into data: URLs (see r793302). |
| { |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| const char kScript[] = R"( |
| let iframe = document.createElement('iframe'); |
| iframe.src = 'data:text/html,contents'; |
| document.body.appendChild(iframe); |
| )"; |
| ExecuteScriptAsync(first_tab, kScript); |
| |
| // Verify that content script has been injected. |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| content::RenderFrameHost* main_frame = first_tab->GetPrimaryMainFrame(); |
| content::RenderFrameHost* child_frame = |
| content::ChildFrameAt(main_frame, 0); |
| ASSERT_TRUE(child_frame); |
| EXPECT_EQ("content script has run", |
| content::EvalJs(main_frame, "document.body.innerText")); |
| EXPECT_EQ("content script has run", |
| content::EvalJs(child_frame, "document.body.innerText")); |
| |
| // Verify that ContentScriptTracker properly covered the new child frame |
| // (and continues to correctly cover the initial frame). |
| // |
| // The verification below is a bit redundant, because `main_frame` and |
| // `child_frame` are currently hosted in the same process, but this kind of |
| // verification is important if 1( we ever consider going back to per-frame |
| // tracking or 2) we start isolating opaque-origin/sandboxed frames into a |
| // separate process (tracked in https://crbug.com/510122). |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *main_frame->GetProcess(), extension->id())); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *child_frame->GetProcess(), extension->id())); |
| } |
| } |
| |
| // Covers detecting content script injection into 'about:blank'. |
| IN_PROC_BROWSER_TEST_F( |
| ContentScriptTrackerBrowserTest, |
| ContentScriptDeclarationInExtensionManifest_AboutBlankPopup) { |
| // Install a test extension. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - Declarative", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": [ "tabs", "<all_urls>" ], |
| "content_scripts": [{ |
| "all_frames": true, |
| "match_about_blank": true, |
| "matches": ["*://bar.com/*"], |
| "js": ["content_script.js"] |
| }] |
| } )"; |
| dir.WriteManifest(kManifestTemplate); |
| dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), R"( |
| document.body.innerText = 'content script has run'; |
| chrome.test.sendMessage('Hello from content script!'); )"); |
| const Extension* extension = LoadExtension(dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Navigate to a test page that *is* covered by `content_scripts.matches` |
| // manifest entry above. |
| content::WebContents* first_tab = nullptr; |
| { |
| GURL injected_url = |
| embedded_test_server()->GetURL("bar.com", "/title1.html"); |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), injected_url)); |
| |
| // Verify that content script has been injected. |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| first_tab = browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_EQ("content script has run", |
| content::EvalJs(first_tab, "document.body.innerText")); |
| |
| // Verify that ContentScriptTracker properly covered the initial frame. |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *first_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| } |
| |
| // Open a new tab with 'about:blank'. This may be tricky, because the initial |
| // 'about:blank' navigation will not go through ReadyToCommit state. |
| { |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| content::WebContentsAddedObserver popup_observer; |
| ASSERT_TRUE(ExecJs(first_tab, "window.open('about:blank', '_blank')")); |
| content::WebContents* popup = popup_observer.GetWebContents(); |
| WaitForLoadStop(popup); |
| |
| // Verify that content script has been injected. |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| EXPECT_EQ("content script has run", |
| content::EvalJs(first_tab, "document.body.innerText")); |
| EXPECT_EQ("content script has run", |
| content::EvalJs(popup, "document.body.innerText")); |
| |
| // Verify that ContentScriptTracker properly covered the popup (and |
| // continues to correctly cover the initial frame). The verification below |
| // is a bit redundant, because `first_tab` and `popup` are hosted in the |
| // same process, but this kind of verification is important if we ever |
| // consider going back to per-frame tracking. |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *first_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *popup->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| } |
| } |
| |
| // Covers detecting content script injection into an initial empty document. |
| // |
| // The code below exercises the test steps from "scenario #3" from the "Tracking |
| // injections in an initial empty document" section of a @chromium.org document |
| // here: |
| // https://docs.google.com/document/d/1MFprp2ss2r9RNamJ7Jxva1bvRZvec3rzGceDGoJ6vW0/edit?usp=sharing |
| IN_PROC_BROWSER_TEST_F( |
| ContentScriptTrackerBrowserTest, |
| ContentScriptDeclarationInExtensionManifest_SubframeWithInitialEmptyDoc) { |
| // Install a test extension. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - Declarative", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": [ "tabs", "<all_urls>" ], |
| "content_scripts": [{ |
| "all_frames": true, |
| "match_about_blank": true, |
| "matches": ["*://bar.com/title1.html"], |
| "js": ["content_script.js"] |
| }] |
| } )"; |
| dir.WriteManifest(kManifestTemplate); |
| dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), R"( |
| var counter = 0; |
| function leaveContentScriptMarker() { |
| const kExpectedText = 'content script has run: '; |
| if (document.body.innerText.startsWith(kExpectedText)) |
| return; |
| |
| counter += 1; |
| document.body.innerText = kExpectedText + counter; |
| chrome.test.sendMessage('Hello from content script!'); |
| } |
| |
| // Leave a content script mark *now*. |
| leaveContentScriptMarker(); |
| |
| // Periodically check if the mark needs to be reinserted (with a new value |
| // of `counter`). This helps to demonstrate (in a test step somewhere |
| // below) that the content script "survives" a `document.open` operation. |
| setInterval(leaveContentScriptMarker, 100); )"); |
| const Extension* extension = LoadExtension(dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Navigate to a test page that *is* covered by `content_scripts.matches` |
| // manifest entry above. |
| content::WebContents* first_tab = nullptr; |
| { |
| GURL injected_url = |
| embedded_test_server()->GetURL("bar.com", "/title1.html"); |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), injected_url)); |
| |
| // Verify that content script has been injected. |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| first_tab = browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_EQ("content script has run: 1", |
| content::EvalJs(first_tab, "document.body.innerText")); |
| |
| // Verify that ContentScriptTracker properly covered the initial frame. |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *first_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| } |
| |
| // Add a new subframe with `src=javascript:...` attribute. This will leave |
| // the subframe at the initial empty document (no navigation / no |
| // ReadyToCommit), but still end up injecting the content script. |
| // |
| // (This is "Step 1" from the doc linked in the comment right above |
| // IN_PROC_BROWSER_TEST_F.) |
| { |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| const char kScript[] = R"( |
| let iframe = document.createElement('iframe'); |
| iframe.name = 'test-child-frame'; |
| iframe.src = 'javascript:"something"'; |
| document.body.appendChild(iframe); |
| )"; |
| ExecuteScriptAsync(first_tab, kScript); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| // Verify expected properties of the test scenario - the `child_frame` should |
| // have stayed at the initial empty document. |
| content::RenderFrameHost* main_frame = first_tab->GetPrimaryMainFrame(); |
| content::RenderFrameHost* child_frame = content::ChildFrameAt(main_frame, 0); |
| ASSERT_TRUE(child_frame); |
| EXPECT_EQ(main_frame->GetLastCommittedOrigin().Serialize(), |
| content::EvalJs(child_frame, "origin")); |
| // Renderer-side and browser-side do not exactly agree on the URL of the child |
| // frame... |
| EXPECT_EQ("about:blank", content::EvalJs(child_frame, "location.href")); |
| EXPECT_EQ(GURL(), child_frame->GetLastCommittedURL()); |
| |
| // Verify that ContentScriptTracker properly covered the new child frame (and |
| // continues to correctly cover the initial frame). The verification below is |
| // a bit redundant, because `main_frame` and `child_frame` are hosted in the |
| // same process, but this kind of verification is important if we ever |
| // consider going back to per-frame tracking. |
| EXPECT_EQ("content script has run: 1", |
| content::EvalJs(main_frame, "document.body.innerText")); |
| EXPECT_EQ("content script has run: 1", |
| content::EvalJs(child_frame, "document.body.innerText")); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *main_frame->GetProcess(), extension->id())); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *child_frame->GetProcess(), extension->id())); |
| |
| // Execute `document.open()` on the initial empty document child frame. The |
| // content script injected previously will survive this (event listeners are |
| // reset but the `setInterval` callback keeps executing). |
| // |
| // This step changes the URL of the `child_frame` (in a same-document |
| // navigation) from "about:blank" to a URL that (unlike the parent) is no |
| // longer covered by the `matches` patterns from the extension manifest. |
| { |
| // Inject a new frame to execute `document.open` from. |
| // |
| // (This is "Step 2" from the doc linked in the comment right above |
| // IN_PROC_BROWSER_TEST_F.) |
| content::TestNavigationObserver nav_observer(first_tab, 1); |
| const char kFrameInsertingScriptTemplate[] = R"( |
| var f = document.createElement('iframe'); |
| f.src = $1; |
| document.body.appendChild(f); |
| )"; |
| GURL non_injected_url = |
| embedded_test_server()->GetURL("bar.com", "/title2.html"); |
| ASSERT_TRUE(content::ExecJs( |
| main_frame, |
| content::JsReplace(kFrameInsertingScriptTemplate, non_injected_url))); |
| nav_observer.Wait(); |
| } |
| content::RenderFrameHost* another_frame = |
| content::ChildFrameAt(main_frame, 1); |
| ASSERT_TRUE(another_frame); |
| { |
| // Execute `document.open`. |
| // |
| // (This is "Step 3" from the doc linked in the comment right above |
| // IN_PROC_BROWSER_TEST_F.) |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| const char kDocumentWritingScript[] = R"( |
| var win = window.open('', 'test-child-frame'); |
| win.document.open(); |
| win.document.close(); |
| )"; |
| ASSERT_TRUE(content::ExecJs(another_frame, kDocumentWritingScript)); |
| |
| // Demonstrate that the original content script has survived "resetting" of |
| // the document. (document.open/write/close triggers a same-document |
| // navigation - it keeps the document/window/RenderFrame[Host]; OTOH we use |
| // setInterval because it is one of few things that survive across such |
| // boundary - in particular all event listeners will be reset.) |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| EXPECT_EQ("content script has run: 2", |
| content::EvalJs(child_frame, "document.body.innerText")); |
| |
| // Demonstrate that `document.open` didn't change the URL of the |
| // `child_frame`. |
| EXPECT_EQ(another_frame->GetLastCommittedURL(), |
| content::EvalJs(child_frame, "location.href")); |
| EXPECT_EQ(GURL(), child_frame->GetLastCommittedURL()); |
| } |
| |
| // Verify that ContentScriptTracker still properly covers both frames. The |
| // verification below is a bit redundant, because `main_frame` and |
| // `child_frame` are hosted in the same process, but this kind of verification |
| // is important if we ever consider going back to per-frame tracking. |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *main_frame->GetProcess(), extension->id())); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *child_frame->GetProcess(), extension->id())); |
| } |
| |
| // This is a regression test for https://crbug.com/1312125 - it simulates a race |
| // where an extension is loaded during or before a navigation, resulting in |
| // ContentScriptTracker::WillUpdateContentScriptsInRenderer getting called |
| // between ReadyToCommit and DidCommit of a navigation from a page where content |
| // scripts are not injected, to a page where content scripts are injected. |
| IN_PROC_BROWSER_TEST_F( |
| ContentScriptTrackerBrowserTest, |
| ContentScriptDeclarationInExtensionManifest_ScriptLoadRacesWithDidCommit) { |
| // Navigate to a test page that is *not* covered by `content_scripts.matches` |
| // manifest entry used in this test (see `kManifestTemplate` below). |
| GURL ignored_url = |
| embedded_test_server()->GetURL("foo.test.com", "/title1.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), ignored_url)); |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // The test uses a long-running `unload` handler to postpone DidCommit in a |
| // same-process, cross-origin navigation that happens in the next test steps: |
| // - "cross-origin" aspect is needed because we need to navigate from a page |
| // not covered by content scripts, into a page covered by content scripts + |
| // because ContentScriptTracker ignores the path part of URL patterns (e.g. |
| // calling `MatchesSecurityOrigin()`). |
| // - "same-process" aspect is needed because we need a same-process navigation |
| // in order to postpone DidCommit IPC (by having an long-running unload |
| // handler). In a typical desktop setting same-site navigations should be |
| // same-process. |
| const char kUnloadHandlerInstallationScript[] = R"( |
| window.addEventListener('unload', function(event) { |
| // BAD CODE - please don't copy&paste. See below for an explanation |
| // why there doesn't seem to a better approach *here* (i.e. see the |
| // comment in a section titled "Orchestrate the race condition"). |
| const sleep_duration = 3000; // milliseconds |
| const start = new Date().getTime(); |
| do { |
| var now = new Date().getTime(); |
| } while (now < (start + sleep_duration)); |
| }); |
| )"; |
| ASSERT_TRUE(content::ExecJs(web_contents, kUnloadHandlerInstallationScript)); |
| |
| // Prepare a test directory, but don't install an extension just yet. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - Declarative", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": [ "tabs", "<all_urls>" ], |
| "content_scripts": [{ |
| "all_frames": true, |
| "match_about_blank": true, |
| "matches": ["*://bar.test.com/*"], |
| "js": ["content_script.js"] |
| }] |
| } )"; |
| dir.WriteManifest(kManifestTemplate); |
| dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), R"( |
| document.body.innerText = 'content script has run'; |
| chrome.test.sendMessage('Hello from content script!'); |
| )"); |
| base::FilePath unpacked_path = dir.UnpackedPath(); |
| |
| // *Initiate* navigation to a test page that *is* covered by |
| // `content_scripts.matches` manifest entry above and use `navigation_manager` |
| // to wait until ReadyToCommit happens, |
| GURL injected_url = |
| embedded_test_server()->GetURL("bar.test.com", "/title1.html"); |
| content::TestNavigationManager navigation_manager(web_contents, injected_url); |
| bool did_commit_has_happened = false; |
| content::CommitMessageDelayer commit_delayer( |
| web_contents, injected_url, |
| base::BindLambdaForTesting([&](content::RenderFrameHost* frame) { |
| // Race step UI.3b (see below). |
| did_commit_has_happened = true; |
| })); |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| ASSERT_TRUE( |
| content::BeginNavigateToURLFromRenderer(web_contents, injected_url)); |
| |
| // Orchestrate the race condition: |
| // *) Race step UI.1: UI thread: |
| // *) UI.1.1: NavigationThrottle pauses the navigation just *before* |
| // ReadyToCommit notifications (when test calls |
| // TestNavigationManager::WaitForResponse). |
| // *) UI.1.2: UI thread: Navigation resumes (when test calls |
| // TestNavigationManager::ResumeNavigation) and |
| // ContentScriptTracker::ReadyToCommitNavigation gets called. |
| // *) UI.1.3: UI thread: Loading of the Chrome Extension starts (when |
| // test calls LoadExtension). |
| // *) Parallel steps: |
| // *) Race step FILE.2: FILE thread: Extension and its content scripts |
| // continue loading (triggered by step UI.1.3 above; see for example |
| // LoadScriptsOnFileTaskRunner in e/b/extension_user_script_loader.cc). |
| // This is a simplification - loading of content scripts is just *one* |
| // of multiple potential thread hops involved in loading an extension. |
| // *) Race step RENDERER.2: Commit IPC is received and handled: |
| // *) RENDERER.2.1, `unload` handler runs |
| // *) RENDERER.2.???, Renderer is notified about newly loaded |
| // extension and its content scripts |
| // *) RENDERER.2.8, `DidCommit` is sent back to the Browser |
| // *) RENDERER.2.9, Content script gets injected (hopefully, |
| // depending on whether step "RENDERER.2.???" happened before) |
| // *) Racey steps where ordering matters for the repro, but where the test |
| // doesn't guarantee the ordering between UI.3a and UI.3b: |
| // *) Race step UI.3a: Task posted by FILE.2 gets run on UI thread. |
| // ContentScriptTracker::WillUpdateContentScriptsInRenderer get called. |
| // *) Race step UI.3b: Task posted by IO.2 gets run on UI thread. |
| // DidCommit happens. |
| // *) Non-racey step UI.4: UI thread: IPC from the content script is |
| // processed. The test simulates this by explicitly calling and checking |
| // ContentScriptTracker::DidProcessRunContentScriptFromExtension which in |
| // presence of https://crbug.com/1312125 could have incorrectly returned |
| // false. |
| // |
| // Triggering https://crbug.com/1312125 requires that UI.3a happens before |
| // UI.3b - when this happens then ContentScriptTracker's |
| // WillUpdateContentScriptsInRenderer won't see the newly committed URL and |
| // won't realize that content script may be injected into the newly committed |
| // document (the fix is to add ContentScriptTracker::DidFinishNavigation). |
| // Additionally, the repro requires that RENDERER.2.??? happens before the |
| // Renderer commits the page. |
| // |
| // The test doesn't guarantee the ordering of UI.3a and UI.3b, but the desired |
| // ordering does happen in practice when running this test (the time from UI.1 |
| // to UI.3a is around 30 milliseconds which is much shorter than 3000 |
| // milliseconds used by the `unload` handler). This is already sufficient and |
| // helpful for verifying the fix for the product code. This is not ideal, but |
| // making the test more robust seems quite difficult - see the discussion in |
| // https://chromium-review.googlesource.com/c/chromium/src/+/3587823/8#message-b4f0abdcc2a6cedf681d33dbe1ddbccc381ad932 |
| ASSERT_TRUE(navigation_manager.WaitForResponse()); // Step UI.1.1 |
| navigation_manager.ResumeNavigation(); // Step UI.1.2 |
| const Extension* extension = LoadExtension(unpacked_path); // Step UI.1.3 |
| ASSERT_TRUE(extension); |
| commit_delayer.Wait(); // Step UI.3b - part1 |
| ASSERT_TRUE( |
| navigation_manager.WaitForNavigationFinished()); // Step UI.3b - part2 |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); // Step UI.4 |
| |
| // Verify that content script has been injected. |
| EXPECT_EQ("content script has run", |
| content::EvalJs(web_contents, "document.body.innerText")); |
| |
| // MAIN VERIFICATION: Verify that ContentScriptTracker detected the injection. |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *web_contents->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| } |
| |
| // Tests tracking of content scripts injected/declared via |
| // `chrome.declarativeContent` API. See also: |
| // https://developer.chrome.com/docs/extensions/reference/declarativeContent/#type-RequestContentScript |
| IN_PROC_BROWSER_TEST_F(ContentScriptTrackerBrowserTest, |
| ContentScriptViaDeclarativeContentApi) { |
| #if BUILDFLAG(IS_MAC) |
| GTEST_SKIP() << "Very flaky on Mac; https://crbug.com/1311017"; |
| #else |
| // Install a test extension. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - Declarative", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": [ "tabs", "<all_urls>", "declarativeContent" ], |
| "background": {"scripts": ["background_script.js"]} |
| } )"; |
| const char kBackgroundScript[] = R"( |
| var rule = { |
| conditions: [ |
| new chrome.declarativeContent.PageStateMatcher({ |
| pageUrl: { hostEquals: 'bar.com', schemes: ['http', 'https'] } |
| }) |
| ], |
| actions: [ new chrome.declarativeContent.RequestContentScript({ |
| js: ["content_script.js"] |
| }) ] |
| }; |
| |
| chrome.runtime.onInstalled.addListener(function(details) { |
| chrome.declarativeContent.onPageChanged.addRules([rule]); |
| }); )"; |
| dir.WriteManifest(kManifestTemplate); |
| dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), kBackgroundScript); |
| const char kContentScript[] = R"( |
| function sendResponse() { |
| document.body.innerText = 'content script has run'; |
| chrome.test.sendMessage('Hello from content script!'); |
| } |
| if (document.readyState === 'complete') |
| sendResponse(); |
| else |
| window.onload = sendResponse; |
| )"; |
| dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), kContentScript); |
| const Extension* extension = LoadExtension(dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Navigate to a test page that is *not* covered by the PageStateMatcher used |
| // above. |
| GURL ignored_url = embedded_test_server()->GetURL("foo.com", "/title1.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), ignored_url)); |
| |
| // Verify that initially no frames show up as having been injected with |
| // content scripts. |
| content::WebContents* first_tab = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_EQ("This page has no title.", |
| content::EvalJs(first_tab, "document.body.innerText")); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *first_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| |
| // Navigate to a test page that *is* covered by the PageStateMatcher above. |
| { |
| GURL injected_url = |
| embedded_test_server()->GetURL("bar.com", "/title1.html"); |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), injected_url, WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| |
| // Verify that content script has been injected. |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| content::WebContents* second_tab = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_NE(first_tab, second_tab); |
| EXPECT_EQ("content script has run", |
| content::EvalJs(second_tab, "document.body.innerText")); |
| |
| // Verify that ContentScriptTracker detected the injection. |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *second_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| } |
| |
| // Verify that still no content script has been run in the `first_tab`. |
| EXPECT_EQ("This page has no title.", |
| content::EvalJs(first_tab, "document.body.innerText")); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *first_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| #endif // BUILDFLAG(IS_MAC) |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ContentScriptTrackerBrowserTest, HistoryPushState) { |
| // Install a test extension. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - Declarative", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": [ "tabs", "<all_urls>" ], |
| "content_scripts": [{ |
| "all_frames": true, |
| "matches": ["*://bar.com/pushed_url.html"], |
| "js": ["content_script.js"], |
| "run_at": "document_end" |
| }] |
| } )"; |
| dir.WriteManifest(kManifestTemplate); |
| dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), R"( |
| document.body.innerText = 'content script has run'; |
| chrome.test.sendMessage('Hello from content script!'); )"); |
| const Extension* extension = LoadExtension(dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| // Navigate to a test page that is *not* covered by the URL patterns above, |
| // but that immediately executes `history.pushState` that changes the URL |
| // to one that *is* covered by the URL patterns above. |
| GURL url = |
| embedded_test_server()->GetURL("bar.com", "/History/push_state.html"); |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| |
| // Verify that content script has been injected. |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| content::RenderFrameHost* main_frame = browser() |
| ->tab_strip_model() |
| ->GetActiveWebContents() |
| ->GetPrimaryMainFrame(); |
| EXPECT_EQ("content script has run", |
| content::EvalJs(main_frame, "document.body.innerText")); |
| |
| // Verify that ContentScriptTracker detected the injection. |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *main_frame->GetProcess(), extension->id())); |
| } |
| |
| class DynamicScriptsTrackerBrowserTest |
| : public ContentScriptTrackerBrowserTest { |
| public: |
| DynamicScriptsTrackerBrowserTest() = default; |
| |
| private: |
| ScopedCurrentChannel current_channel_{version_info::Channel::UNKNOWN}; |
| }; |
| |
| // Tests tracking of content scripts injected/declared via `chrome.scripting` |
| // API. |
| IN_PROC_BROWSER_TEST_F(DynamicScriptsTrackerBrowserTest, |
| ContentScriptViaScriptingApi) { |
| // Install a test extension. |
| TestExtensionDir dir; |
| const char kManifestTemplate[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - ScriptingAPI", |
| "version": "1.0", |
| "manifest_version": 3, |
| "permissions": [ "scripting" ], |
| "host_permissions": ["*://*/*"], |
| "background": { "service_worker": "worker.js" } |
| } )"; |
| const char kWorkerScript[] = R"( |
| var scripts = [{ |
| id: 'script1', |
| matches: ['*://a.com/*'], |
| js: ['content_script.js'], |
| runAt: 'document_end' |
| }]; |
| |
| chrome.runtime.onInstalled.addListener(function(details) { |
| chrome.scripting.registerContentScripts(scripts, () => { |
| chrome.test.sendMessage('SCRIPT_LOADED'); |
| }); |
| }); )"; |
| dir.WriteManifest(kManifestTemplate); |
| dir.WriteFile(FILE_PATH_LITERAL("worker.js"), kWorkerScript); |
| const char kContentScript[] = R"( |
| window.onload = function() { |
| chrome.test.assertEq('complete', document.readyState); |
| document.body.innerText = 'content script has run'; |
| chrome.test.sendMessage('SCRIPT_INJECTED'); |
| } |
| )"; |
| dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), kContentScript); |
| |
| ExtensionTestMessageListener script_loaded_listener("SCRIPT_LOADED"); |
| const Extension* extension = LoadExtension(dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| ASSERT_TRUE(script_loaded_listener.WaitUntilSatisfied()); |
| |
| // Navigate to a test page that is *not* covered by the dynamic content script |
| // used above. |
| GURL ignored_url = embedded_test_server()->GetURL("foo.com", "/title1.html"); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), ignored_url)); |
| |
| // Verify that initially no frames show up as having been injected with |
| // content scripts. |
| content::WebContents* first_tab = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_EQ("This page has no title.", |
| content::EvalJs(first_tab, "document.body.innerText")); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *first_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| |
| // Navigate to a test page that *is* covered by the dynamic content script |
| // above. |
| { |
| GURL injected_url = embedded_test_server()->GetURL("a.com", "/title1.html"); |
| ExtensionTestMessageListener listener("SCRIPT_INJECTED"); |
| ui_test_utils::NavigateToURLWithDisposition( |
| browser(), injected_url, WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| content::WebContents* second_tab = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_NE(first_tab, second_tab); |
| EXPECT_NE(first_tab->GetPrimaryMainFrame()->GetProcess(), |
| second_tab->GetPrimaryMainFrame()->GetProcess()); |
| |
| // Verify that the new tab shows up as having been injected with content |
| // scripts. |
| EXPECT_EQ("content script has run", |
| content::EvalJs(second_tab, "document.body.innerText")); |
| EXPECT_EQ("This page has no title.", |
| content::EvalJs(first_tab, "document.body.innerText")); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *second_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *first_tab->GetPrimaryMainFrame()->GetProcess(), extension->id())); |
| } |
| |
| class ContentScriptTrackerAppBrowserTest : public PlatformAppBrowserTest { |
| public: |
| ContentScriptTrackerAppBrowserTest() = default; |
| |
| void SetUpOnMainThread() override { |
| PlatformAppBrowserTest::SetUpOnMainThread(); |
| |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| content::SetupCrossSiteRedirector(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| } |
| }; |
| |
| // Tests that ContentScriptTracker detects content scripts injected via |
| // <webview> (aka GuestView) APIs. This test covers a basic injection scenario. |
| IN_PROC_BROWSER_TEST_F(ContentScriptTrackerAppBrowserTest, |
| WebViewContentScript) { |
| // Install an unrelated test extension (for testing that ContentScriptTracker |
| // doesn't think that *all* extensions are injecting scripts into a webView). |
| TestExtensionDir unrelated_dir; |
| const char kUnrelatedManifest[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - Unrelated", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": [ "tabs", "<all_urls>" ], |
| "content_scripts": [{ |
| "all_frames": true, |
| "matches": ["*://bar.com/*"], |
| "js": ["content_script.js"], |
| "run_at": "document_start" |
| }] |
| } )"; |
| unrelated_dir.WriteManifest(kUnrelatedManifest); |
| unrelated_dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), R"( |
| chrome.test.sendMessage('Hello from extension content script!'); )"); |
| const Extension* unrelated_extension = |
| LoadExtension(unrelated_dir.UnpackedPath()); |
| ASSERT_TRUE(unrelated_extension); |
| |
| // Load the test app. |
| TestExtensionDir dir; |
| const char kManifest[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - App", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": ["*://*/*", "webview"], |
| "app": { |
| "background": { |
| "scripts": ["background_script.js"] |
| } |
| } |
| } )"; |
| 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); |
| |
| // Launch the test app and grab its WebContents. |
| const Extension* app = LoadAndLaunchApp(dir.UnpackedPath()); |
| ASSERT_TRUE(app); |
| content::WebContents* app_contents = GetFirstAppWindowWebContents(); |
| ASSERT_TRUE(content::WaitForLoadStop(app_contents)); |
| |
| // Navigate the <webview> tag and grab the `guest_process`. |
| content::WebContents* guest_contents = nullptr; |
| { |
| const char kWebViewInjectionScriptTemplate[] = R"( |
| document.querySelector('#webview-tag-container').innerHTML = |
| '<webview style="width: 100px; height: 100px;"></webview>'; |
| var webview = document.querySelector('webview'); |
| webview.src = $1; |
| )"; |
| GURL guest_url1(embedded_test_server()->GetURL("foo.com", "/title1.html")); |
| |
| content::WebContentsAddedObserver guest_contents_observer; |
| ASSERT_TRUE(ExecJs( |
| app_contents, |
| content::JsReplace(kWebViewInjectionScriptTemplate, guest_url1))); |
| guest_contents = guest_contents_observer.GetWebContents(); |
| } |
| |
| // Verify that ContentScriptTracker correctly shows that no content scripts |
| // got injected just yet. |
| content::RenderProcessHost* guest_process = |
| guest_contents->GetPrimaryMainFrame()->GetProcess(); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *guest_process, app->id())); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *guest_process, unrelated_extension->id())); |
| |
| // Declare content scripts + trigger their injection with another navigation. |
| // |
| // TODO(lukasza): Ideally the URL pattern would be more restrictive for the |
| // content script `matches` below (to enable testing whether the target of |
| // navigation URL actually matched the pattern from the `addContentScripts` |
| // call). |
| { |
| const char kContentScriptDeclarationScriptTemplate[] = R"( |
| var webview = document.querySelector('webview'); |
| webview.addContentScripts([{ |
| name: 'rule', |
| matches: ['*://*/*'], |
| js: { code: $1 }, |
| run_at: 'document_start'}]); |
| webview.src = $2; |
| )"; |
| const char kContentScript[] = R"( |
| chrome.test.sendMessage("Hello from webView content script!"); |
| )"; |
| GURL guest_url2(embedded_test_server()->GetURL("bar.com", "/title2.html")); |
| |
| ExtensionTestMessageListener app_script_listener( |
| "Hello from webView content script!"); |
| ExtensionTestMessageListener unrelated_extension_script_listener( |
| "Hello from extension content script!"); |
| content::TestNavigationObserver nav_observer(guest_contents); |
| content::ExecuteScriptAsync( |
| app_contents, |
| content::JsReplace(kContentScriptDeclarationScriptTemplate, |
| kContentScript, guest_url2)); |
| |
| // Wait for the navigation to complete and verify via `listener` that the |
| // expected content script has run. |
| nav_observer.Wait(); |
| ASSERT_TRUE(app_script_listener.WaitUntilSatisfied()); |
| EXPECT_FALSE(unrelated_extension_script_listener.was_satisfied()); |
| } |
| |
| // Verify that ContentScriptTracker detected the content script injection |
| // from `app` in the bar.com guest process (but not from |
| // `unrelated_extension`). |
| guest_process = guest_contents->GetPrimaryMainFrame()->GetProcess(); |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *guest_process, app->id())); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *guest_process, unrelated_extension->id())); |
| } |
| |
| // Tests that ContentScriptTracker detects content scripts injected via |
| // <webview> (aka GuestView) APIs. This test covers a scenario where the |
| // `addContentScripts` API is called in the middle of the test - after |
| // a matching guest content has already loaded (no content scripts there) |
| // but before a matching about:blank guest navigation happens (need to detect |
| // content scripts there). |
| IN_PROC_BROWSER_TEST_F(ContentScriptTrackerAppBrowserTest, |
| WebViewContentScriptForLateAboutBlank) { |
| // Load the test app. |
| TestExtensionDir dir; |
| const char kManifest[] = R"( |
| { |
| "name": "ContentScriptTrackerBrowserTest - App", |
| "version": "1.0", |
| "manifest_version": 2, |
| "permissions": ["*://*/*", "webview"], |
| "app": { |
| "background": { |
| "scripts": ["background_script.js"] |
| } |
| } |
| } )"; |
| 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); |
| |
| // Launch the test app and grab its WebContents. |
| const Extension* app = LoadAndLaunchApp(dir.UnpackedPath()); |
| ASSERT_TRUE(app); |
| content::WebContents* app_contents = GetFirstAppWindowWebContents(); |
| ASSERT_TRUE(content::WaitForLoadStop(app_contents)); |
| |
| // Navigate the <webview> tag and grab the `guest_process`. |
| content::WebContents* guest_contents = nullptr; |
| { |
| const char kWebViewInjectionScriptTemplate[] = R"( |
| document.querySelector('#webview-tag-container').innerHTML = |
| '<webview style="width: 100px; height: 100px;"></webview>'; |
| var webview = document.querySelector('webview'); |
| webview.src = $1; |
| )"; |
| GURL guest_url1(embedded_test_server()->GetURL("foo.com", "/title1.html")); |
| |
| content::WebContentsAddedObserver guest_contents_observer; |
| ASSERT_TRUE(ExecJs( |
| app_contents, |
| content::JsReplace(kWebViewInjectionScriptTemplate, guest_url1))); |
| guest_contents = guest_contents_observer.GetWebContents(); |
| |
| // Wait until the "document_end" timepoint is reached. (Since this is done |
| // before the `addContentScripts` call below, it means that no content |
| // scripts will get injected into the initial document.) |
| EXPECT_TRUE(WaitForLoadStop(guest_contents)); |
| } |
| |
| // Verify that ContentScriptTracker correctly shows that no content scripts |
| // got injected just yet. |
| content::RenderProcessHost* guest_process = |
| guest_contents->GetPrimaryMainFrame()->GetProcess(); |
| EXPECT_FALSE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *guest_process, app->id())); |
| |
| // Declare content scripts and wait until they have been loaded (and |
| // communicated to the renderer process). |
| { |
| const char kContentScriptDeclarationScriptTemplate[] = R"( |
| var webview = document.querySelector('webview'); |
| webview.addContentScripts([{ |
| name: 'rule', |
| all_frames: true, |
| match_about_blank: true, |
| matches: ['*://foo.com/*'], |
| js: { code: $1 }, |
| run_at: 'document_end'}]); |
| )"; |
| const char kContentScript[] = R"( |
| chrome.test.sendMessage("Hello from content script!"); |
| )"; |
| std::string script = content::JsReplace( |
| kContentScriptDeclarationScriptTemplate, kContentScript); |
| |
| UserScriptManager* user_script_manager = |
| ExtensionSystem::Get(guest_process->GetBrowserContext()) |
| ->user_script_manager(); |
| ExtensionUserScriptLoader* user_script_loader = |
| user_script_manager->GetUserScriptLoaderForExtension(app->id()); |
| ContentScriptLoadWaiter content_script_load_waiter(user_script_loader); |
| |
| content::ExecuteScriptAsync(app_contents, script); |
| content_script_load_waiter.Wait(); |
| } |
| |
| // Create an about:blank subframe where content script should get injected |
| // into. |
| { |
| ExtensionTestMessageListener listener("Hello from content script!"); |
| content::TestNavigationObserver nav_observer(guest_contents); |
| const char kAboutBlankScript[] = R"( |
| var f = document.createElement('iframe'); |
| f.src = 'about:blank'; |
| document.body.appendChild(f); |
| )"; |
| content::ExecuteScriptAsync(guest_contents, kAboutBlankScript); |
| |
| // Wait for the navigation to complete and verify via `listener` that the |
| // content script has run. |
| nav_observer.Wait(); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| // Verify that ContentScriptTracker detected the content script injection. |
| EXPECT_TRUE(ContentScriptTracker::DidProcessRunContentScriptFromExtension( |
| *guest_process, app->id())); |
| } |
| |
| } // namespace extensions |