Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(813)

Unified Diff: chrome/browser/password_manager/credential_manager_browsertest.cc

Issue 2947413002: Restrict CM API interface request and message dispatch. (Closed)
Patch Set: Address nit from clamy@. Created 3 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/browser/password_manager/credential_manager_browsertest.cc
diff --git a/chrome/browser/password_manager/credential_manager_browsertest.cc b/chrome/browser/password_manager/credential_manager_browsertest.cc
index f33470ed1a9c5dbc5b503a20f16e33f01cda07bc..3d7364597185a01c83ce41062db818d1f85669ec 100644
--- a/chrome/browser/password_manager/credential_manager_browsertest.cc
+++ b/chrome/browser/password_manager/credential_manager_browsertest.cc
@@ -4,16 +4,21 @@
#include "base/macros.h"
#include "base/stl_util.h"
+#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/password_manager/chrome_password_manager_client.h"
#include "chrome/browser/password_manager/password_manager_test_base.h"
#include "chrome/browser/password_manager/password_store_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_io_data.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/passwords/passwords_model_delegate.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/password_manager/core/browser/password_bubble_experiment.h"
#include "components/password_manager/core/browser/test_password_store.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
@@ -31,8 +36,8 @@ class CredentialManagerBrowserTest : public PasswordManagerBrowserTestBase {
}
bool IsShowingAccountChooser() {
- return PasswordsModelDelegateFromWebContents(WebContents())->
- GetState() == password_manager::ui::CREDENTIAL_REQUEST_STATE;
+ return PasswordsModelDelegateFromWebContents(WebContents())->GetState() ==
+ password_manager::ui::CREDENTIAL_REQUEST_STATE;
}
// Similarly to PasswordManagerBrowserTestBase::NavigateToFile this is a
@@ -48,6 +53,178 @@ class CredentialManagerBrowserTest : public PasswordManagerBrowserTestBase {
observer.Wait();
}
+ // Triggers a call to `navigator.credentials.get` to retrieve passwords, waits
+ // for success, and ASSERTs that |expect_has_results| is satisfied.
+ void TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(
+ content::WebContents* web_contents,
+ bool expect_has_results) {
+ bool result = false;
+ ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
+ web_contents,
+ "navigator.credentials.get({password: true}).then(c => {"
+ " window.domAutomationController.send(!!c);"
+ "});",
+ &result));
+ ASSERT_EQ(expect_has_results, result);
+ }
+
+ // Schedules a call to be made to navigator.credentials.store() in the
+ // `unload` handler to save a credential with |username| and |password|.
+ void ScheduleNavigatorStoreCredentialAtUnload(
+ content::WebContents* web_contents,
+ const char* username,
+ const char* password) {
+ ASSERT_TRUE(content::ExecuteScript(
+ web_contents,
+ base::StringPrintf(
+ "window.addEventListener(\"unload\", () => {"
+ " var c = new PasswordCredential({ id: '%s', password: '%s' });"
+ " navigator.credentials.store(c);"
+ "});",
+ username, password)));
+ }
+
+ // Tests that when navigator.credentials.store() is called in an `unload`
+ // handler before a same-RenderFrame navigation, the request is guaranteed to
+ // be serviced in the context of the initial document.
+ //
+ // If |preestablish_mojo_pipe| is set, then the CredentialManagerClient will
+ // establish the Mojo connection to the CredentialManagerImpl ahead of time,
+ // instead of letting the Mojo connection be established on-demand when the
+ // call to store() triggered from the unload handler.
+ void TestStoreInUnloadHandlerForSameSiteNavigation(
+ bool preestablish_mojo_pipe) {
+ // Use URLs that differ on subdomains so we can tell which one was used for
+ // saving, but they still belong to the same SiteInstance, so they will be
+ // renderered in the same RenderFrame (in the same process).
+ const GURL a_url1 = https_test_server().GetURL("foo.a.com", "/title1.html");
+ const GURL a_url2 = https_test_server().GetURL("bar.a.com", "/title2.html");
+
+ // Navigate to a mostly empty page.
+ ui_test_utils::NavigateToURL(browser(), a_url1);
+
+ ChromePasswordManagerClient* client =
+ ChromePasswordManagerClient::FromWebContents(WebContents());
+
+ if (preestablish_mojo_pipe) {
+ EXPECT_FALSE(client->has_binding_for_credential_manager());
+ ASSERT_NO_FATAL_FAILURE(
+ TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(
+ WebContents(), false));
+ EXPECT_TRUE(client->has_binding_for_credential_manager());
+ }
+
+ // Schedule storing a credential on the `unload` event.
+ ASSERT_NO_FATAL_FAILURE(ScheduleNavigatorStoreCredentialAtUnload(
+ WebContents(), "user", "hunter2"));
+
+ // Trigger a same-site navigation carried out in the same RenderFrame.
+ content::RenderFrameHost* old_rfh = WebContents()->GetMainFrame();
+ ui_test_utils::NavigateToURL(browser(), a_url2);
+ ASSERT_EQ(old_rfh, WebContents()->GetMainFrame());
+
+ // Ensure that the old document no longer has a Mojo connection to the
+ // CredentialManagerImpl, nor can it get one later.
+ //
+ // The sequence of events for same-RFH navigations is as follows:
+ // 1.) FrameHostMsg_DidStartProvisionalLoad
+ // 2.) FrameLoader::PrepareForCommit
+ // 2.1) Document::Shutdown (old Document)
+ // 3.) FrameHostMsg_DidCommitProvisionalLoad (new load)
+ // ... loading ...
+ // 4.) FrameHostMsg_DidStopLoading
+ // 5.) content::WaitForLoadStop inside NavigateToURL returns
+ // 6.) NavigateToURL returns
+ //
+ // After Step 2.1, the old Document cannot issue a new Mojo InterfaceRequest
+ // anymore. Plus, because the AssociatedInterfaceRegistry, through which the
+ // associated interface to the CredentialManagerImpl is retrieved, is itself
+ // Channel-associated, any InterfaceRequest messages that may have been
+ // issued before or during Step 2.1, will be guaranteed to arrive to the
+ // browser side before FrameHostMsg_DidCommitProvisionalLoad in Step 3.
+ //
+ // Hence it is sufficient to check that the Mojo connection is closed now.
+ EXPECT_FALSE(client->has_binding_for_credential_manager());
+
+ // Ensure that the navigator.credentials.store() call was serviced in the
+ // context of the old URL, |a_url|.
+ //
+ // The CredentialManager Mojo interface is Channel-associated, so message
+ // ordering with legacy IPC messages is preserved. Therefore, servicing the
+ // store() called from the `unload` handler, triggered from
+ // FrameLoader::PrepareForCommit, will be serviced before
+ // FrameHostMsg_DidCommitProvisionalLoad, thus before DidFinishNavigation,
+ ASSERT_TRUE(client->was_store_ever_called());
+
+ BubbleObserver prompt_observer(WebContents());
+ prompt_observer.WaitForSavePrompt();
+ ASSERT_TRUE(prompt_observer.IsShowingSavePrompt());
+ prompt_observer.AcceptSavePrompt();
+
+ WaitForPasswordStore();
+
+ password_manager::TestPasswordStore* test_password_store =
+ static_cast<password_manager::TestPasswordStore*>(
+ PasswordStoreFactory::GetForProfile(
+ browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
+ .get());
+
+ ASSERT_EQ(1u, test_password_store->stored_passwords().size());
+ autofill::PasswordForm signin_form =
+ test_password_store->stored_passwords().begin()->second[0];
+ EXPECT_EQ(base::ASCIIToUTF16("user"), signin_form.username_value);
+ EXPECT_EQ(base::ASCIIToUTF16("hunter2"), signin_form.password_value);
+ EXPECT_EQ(a_url1.GetOrigin(), signin_form.origin);
+ }
+
+ // Tests the when navigator.credentials.store() is called in an `unload`
+ // handler before a cross-site transfer navigation, the request is ignored.
+ //
+ // If |preestablish_mojo_pipe| is set, then the CredentialManagerClient will
+ // establish the Mojo connection to the CredentialManagerImpl ahead of time,
+ // instead of letting the Mojo connection be established on-demand when the
+ // call to store() triggered from the unload handler.
+ void TestStoreInUnloadHandlerForCrossSiteNavigation(
+ bool preestablish_mojo_pipe) {
+ const GURL a_url = https_test_server().GetURL("a.com", "/title1.html");
+ const GURL b_url = https_test_server().GetURL("b.com", "/title2.html");
+
+ // Navigate to a mostly empty page.
+ ui_test_utils::NavigateToURL(browser(), a_url);
+
+ ChromePasswordManagerClient* client =
+ ChromePasswordManagerClient::FromWebContents(WebContents());
+
+ if (preestablish_mojo_pipe) {
+ EXPECT_FALSE(client->has_binding_for_credential_manager());
+ ASSERT_NO_FATAL_FAILURE(
+ TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(
+ WebContents(), false));
+ EXPECT_TRUE(client->has_binding_for_credential_manager());
+ }
+
+ // Schedule storing a credential on the `unload` event.
+ ASSERT_NO_FATAL_FAILURE(ScheduleNavigatorStoreCredentialAtUnload(
+ WebContents(), "user", "hunter2"));
+
+ // Trigger a cross-site navigation that is carried out in a new renderer,
+ // and which will swap out the old RenderFrameHost.
+ content::RenderFrameDeletedObserver rfh_destruction_observer(
+ WebContents()->GetMainFrame());
+ ui_test_utils::NavigateToURL(browser(), b_url);
+
+ // Ensure that the navigator.credentials.store() call is never serviced.
+ // The sufficient conditions for this are:
+ // -- The swapped out RFH is destroyed, so the RenderFrame cannot
+ // establish a new Mojo connection to CredentialManagerImpl anymore.
+ // -- There is no already existing Mojo connection to CredentialManagerImpl
+ // either, which could be used to call store() in the future.
+ // -- There have not been any calls to store() in the past.
+ rfh_destruction_observer.WaitUntilDeleted();
+ EXPECT_FALSE(client->has_binding_for_credential_manager());
+ EXPECT_FALSE(client->was_store_ever_called());
+ }
+
private:
DISALLOW_COPY_AND_ASSIGN(CredentialManagerBrowserTest);
};
@@ -60,7 +237,8 @@ IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
scoped_refptr<password_manager::TestPasswordStore> password_store =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
- browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS).get());
+ browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
+ .get());
autofill::PasswordForm signin_form;
signin_form.signon_realm = embedded_test_server()->base_url().spec();
signin_form.password_value = base::ASCIIToUTF16("password");
@@ -71,8 +249,8 @@ IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
NavigateToFile("/password/password_form.html");
std::string fill_password =
- "document.getElementById('username_field').value = 'user';"
- "document.getElementById('password_field').value = 'password';";
+ "document.getElementById('username_field').value = 'user';"
+ "document.getElementById('password_field').value = 'password';";
ASSERT_TRUE(content::ExecuteScript(RenderViewHost(), fill_password));
// Call the API to trigger the notification to the client.
@@ -82,9 +260,10 @@ IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
".then(cred => window.location = '/password/done.html')"));
// Mojo calls from the renderer are asynchronous.
BubbleObserver(WebContents()).WaitForAccountChooser();
- PasswordsModelDelegateFromWebContents(WebContents())->ChooseCredential(
- signin_form,
- password_manager::CredentialType::CREDENTIAL_TYPE_PASSWORD);
+ PasswordsModelDelegateFromWebContents(WebContents())
+ ->ChooseCredential(
+ signin_form,
+ password_manager::CredentialType::CREDENTIAL_TYPE_PASSWORD);
NavigationObserver observer(WebContents());
observer.SetPathToWaitFor("/password/done.html");
@@ -245,6 +424,106 @@ IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
EXPECT_FALSE(prompt_observer.IsShowingSavePrompt());
}
+// Regression test for https://crbug.com/736357.
+IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
+ StoreInUnloadHandler_SameSite_OnDemandMojoPipe) {
+ TestStoreInUnloadHandlerForSameSiteNavigation(
+ false /* preestablish_mojo_pipe */);
+}
+
+// Regression test for https://crbug.com/736357.
+IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
+ StoreInUnloadHandler_SameSite_PreestablishedPipe) {
+ TestStoreInUnloadHandlerForSameSiteNavigation(
+ true /* preestablish_mojo_pipe */);
+}
+// Regression test for https://crbug.com/736357.
+IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
+ StoreInUnloadHandler_CrossSite_OnDemandMojoPipe) {
+ TestStoreInUnloadHandlerForCrossSiteNavigation(
+ false /* preestablish_mojo_pipe */);
+}
+
+// Regression test for https://crbug.com/736357.
+IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
+ StoreInUnloadHandler_CrossSite_PreestablishedPipe) {
+ TestStoreInUnloadHandlerForCrossSiteNavigation(
+ true /* preestablish_mojo_pipe */);
+}
+
+// Regression test for https://crbug.com/736357.
+IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
+ MojoConnectionRecreatedAfterNavigation) {
+ const GURL a_url1 = https_test_server().GetURL("a.com", "/title1.html");
+ const GURL a_url2 = https_test_server().GetURL("a.com", "/title2.html");
+ const GURL a_url2_ref = https_test_server().GetURL("a.com", "/title2.html#r");
+ const GURL b_url = https_test_server().GetURL("b.com", "/title2.html#ref");
+
+ // Enable 'auto signin' for the profile.
+ password_bubble_experiment::RecordAutoSignInPromptFirstRunExperienceWasShown(
+ browser()->profile()->GetPrefs());
+
+ // Navigate to a mostly empty page.
+ ui_test_utils::NavigateToURL(browser(), a_url1);
+
+ ChromePasswordManagerClient* client =
+ ChromePasswordManagerClient::FromWebContents(WebContents());
+
+ // Store a credential, and expect it to establish the Mojo connection.
+ EXPECT_FALSE(client->has_binding_for_credential_manager());
+ EXPECT_FALSE(client->was_store_ever_called());
+
+ ASSERT_TRUE(content::ExecuteScript(
+ WebContents(),
+ "var c = new PasswordCredential({ id: 'user', password: 'hunter2' });"
+ "navigator.credentials.store(c);"));
+
+ BubbleObserver prompt_observer(WebContents());
+ prompt_observer.WaitForSavePrompt();
+ ASSERT_TRUE(prompt_observer.IsShowingSavePrompt());
+ prompt_observer.AcceptSavePrompt();
+ WaitForPasswordStore();
+
+ EXPECT_TRUE(client->has_binding_for_credential_manager());
+ EXPECT_TRUE(client->was_store_ever_called());
+
+ // Trigger a same-site navigation.
+ content::RenderFrameHost* old_rfh = WebContents()->GetMainFrame();
+ ui_test_utils::NavigateToURL(browser(), a_url2);
+ ASSERT_EQ(old_rfh, WebContents()->GetMainFrame());
+
+ // Expect the Mojo connection closed.
+ EXPECT_FALSE(client->has_binding_for_credential_manager());
+
+ // Calling navigator.credentials.get() again should re-establish the Mojo
+ // connection and succeed.
+ ASSERT_NO_FATAL_FAILURE(
+ TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(WebContents(),
+ true));
+ EXPECT_TRUE(client->has_binding_for_credential_manager());
+
+ // Same-document navigation. Call to get() succeeds.
+ ui_test_utils::NavigateToURL(browser(), a_url2_ref);
+ ASSERT_EQ(old_rfh, WebContents()->GetMainFrame());
+ EXPECT_TRUE(client->has_binding_for_credential_manager());
+ ASSERT_NO_FATAL_FAILURE(
+ TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(WebContents(),
+ true));
+
+ // Cross-site navigation. Call to get() succeeds without results.
+ ui_test_utils::NavigateToURL(browser(), b_url);
+ ASSERT_NO_FATAL_FAILURE(
+ TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(WebContents(),
+ false));
+
+ // Trigger a cross-site navigation back. Call to get() should still succeed,
+ // and once again with results.
+ ui_test_utils::NavigateToURL(browser(), a_url1);
+ ASSERT_NO_FATAL_FAILURE(
+ TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(WebContents(),
+ true));
+}
+
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest, SaveViaAPIAndAutofill) {
NavigateToFile("/password/password_form.html");

Powered by Google App Engine
This is Rietveld 408576698