| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <optional> |
| #include <string> |
| |
| #include "base/command_line.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/strings/strcat.h" |
| #include "base/test/bind.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/time/time.h" |
| #include "content/browser/bad_message.h" |
| #include "content/browser/renderer_host/frame_tree.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/site_isolation_policy.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_content_browser_client.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/test_devtools_protocol_client.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "ipc/ipc_security_test_util.h" |
| #include "net/base/features.h" |
| #include "net/base/filename_util.h" |
| #include "net/cookies/canonical_cookie.h" |
| #include "net/cookies/canonical_cookie_test_helpers.h" |
| #include "net/cookies/cookie_access_result.h" |
| #include "net/cookies/cookie_util.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/http/alternative_service.h" |
| #include "net/storage_access_api/status.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/network_switches.h" |
| #include "services/network/public/mojom/restricted_cookie_manager.mojom-test-utils.h" |
| #include "services/network/public/mojom/restricted_cookie_manager.mojom.h" |
| #include "services/service_manager/public/cpp/interface_provider.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/features_generated.h" |
| #include "url/gurl.h" |
| |
| using testing::IsEmpty; |
| using testing::Key; |
| using testing::UnorderedElementsAre; |
| |
| namespace content { |
| |
| namespace { |
| |
| void EnableDevtoolsThirdPartyCookieRestriction( |
| TestDevToolsProtocolClient& frame_devtools_client) { |
| base::Value::Dict command_params; |
| frame_devtools_client.SendCommandSync("Network.enable"); |
| command_params.Set("enableThirdPartyCookieRestriction", true); |
| command_params.Set("disableThirdPartyCookieMetadata", false); |
| command_params.Set("disableThirdPartyCookieHeuristics", false); |
| frame_devtools_client.SendCommandAsync("Network.setCookieControls", |
| std::move(command_params)); |
| } |
| |
| void SetCookieFromJS(RenderFrameHost* frame, std::string cookie) { |
| EvalJsResult result = EvalJs(frame, "document.cookie = '" + cookie + "'"); |
| EXPECT_TRUE(result.error.empty()) << result.error; |
| } |
| |
| std::string GetCookieFromJS(RenderFrameHost* frame) { |
| return EvalJs(frame, "document.cookie;").ExtractString(); |
| } |
| |
| void SetCookieDirect(WebContentsImpl* tab, |
| const GURL& url, |
| const std::string& cookie_line) { |
| net::CookieOptions options; |
| // Allow setting SameSite cookies. |
| options.set_same_site_cookie_context( |
| net::CookieOptions::SameSiteCookieContext::MakeInclusive()); |
| |
| auto cookie_obj = net::CanonicalCookie::CreateForTesting(url, cookie_line, |
| base::Time::Now()); |
| |
| base::RunLoop run_loop; |
| tab->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetCookieManagerForBrowserProcess() |
| ->SetCanonicalCookie( |
| *cookie_obj, url, options, |
| base::BindLambdaForTesting( |
| [&](net::CookieAccessResult status) { run_loop.Quit(); })); |
| run_loop.Run(); |
| } |
| |
| std::string GetCookiesDirect(WebContentsImpl* tab, const GURL& url) { |
| net::CookieOptions options; |
| // Allow setting SameSite cookies. |
| options.set_same_site_cookie_context( |
| net::CookieOptions::SameSiteCookieContext::MakeInclusive()); |
| net::CookieList result; |
| base::RunLoop run_loop; |
| tab->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetCookieManagerForBrowserProcess() |
| ->GetCookieList( |
| url, options, net::CookiePartitionKeyCollection(), |
| base::BindLambdaForTesting( |
| [&](const net::CookieAccessResultList& cookie_list, |
| const net::CookieAccessResultList& excluded_cookies) { |
| result = net::cookie_util::StripAccessResults(cookie_list); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| return net::CanonicalCookie::BuildCookieLine(result); |
| } |
| |
| } // namespace |
| |
| class CookieBrowserTest |
| : public ContentBrowserTest, |
| public ::testing::WithParamInterface<std::tuple<bool, bool>> { |
| public: |
| CookieBrowserTest() |
| : https_server_(net::test_server::EmbeddedTestServer::TYPE_HTTPS) { |
| scoped_feature_list_.InitWithFeatureStates( |
| {{network::features::kGetCookiesOnSet, GetCookiesOnSetEnabled()}, |
| {blink::features::kAsyncSetCookie, AsyncSetCookieEnabled()}}); |
| } |
| ~CookieBrowserTest() override = default; |
| |
| protected: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| command_line->AppendSwitch( |
| switches::kEnableExperimentalWebPlatformFeatures); |
| } |
| |
| void SetUpOnMainThread() override { |
| // Support multiple sites on the test server. |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| https_server_.AddDefaultHandlers(GetTestDataFilePath()); |
| https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); |
| SetupCrossSiteRedirector(&https_server_); |
| ASSERT_TRUE(https_server_.Start()); |
| } |
| |
| bool GetCookiesOnSetEnabled() { return std::get<0>(GetParam()); } |
| |
| bool AsyncSetCookieEnabled() { return std::get<1>(GetParam()); } |
| |
| net::test_server::EmbeddedTestServer https_server_; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| , |
| CookieBrowserTest, |
| testing::Combine(testing::Bool(), testing::Bool()), |
| [](const testing::TestParamInfo<std::tuple<bool, bool>>& info) { |
| std::string name = |
| std::get<0>(info.param) ? "GetOnSetEnabled" : "GetOnSetDisabled"; |
| name += "_"; |
| name += std::get<1>(info.param) ? "Async" : "Sync"; |
| return name; |
| }); |
| |
| // Exercises basic cookie operations via javascript, including an http page |
| // interacting with secure cookies. |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, Cookies) { |
| SetupCrossSiteRedirector(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // The server sends a HttpOnly cookie. The RestrictedCookieManager should |
| // never allow this to be sent to any renderer process. |
| GURL https_url = |
| https_server_.GetURL("a.test", "/set-cookie?notforjs=1;HttpOnly"); |
| GURL http_url = |
| embedded_test_server()->GetURL("a.test", "/frame_with_load_event.html"); |
| |
| Shell* shell2 = CreateBrowser(); |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| EXPECT_TRUE(NavigateToURL(shell2, https_url)); |
| |
| WebContentsImpl* web_contents_https = |
| static_cast<WebContentsImpl*>(shell2->web_contents()); |
| WebContentsImpl* web_contents_http = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| if (AreStrictSiteInstancesEnabled()) { |
| EXPECT_EQ("http://a.test/", |
| web_contents_http->GetSiteInstance()->GetSiteURL().spec()); |
| // Create expected site url, including port if origin isolation is enabled. |
| std::string expected_site_url = |
| SiteIsolationPolicy::AreOriginKeyedProcessesEnabledByDefault() |
| ? url::Origin::Create(https_url).GetURL().spec() |
| : std::string("https://a.test/"); |
| EXPECT_EQ(expected_site_url, |
| web_contents_https->GetSiteInstance()->GetSiteURL().spec()); |
| } else { |
| // Note: Both use the default SiteInstance because the URLs don't require |
| // a dedicated process, but these default SiteInstances are not the same |
| // object because they come from different BrowsingInstances. |
| EXPECT_TRUE(web_contents_http->GetSiteInstance()->IsDefaultSiteInstance()); |
| EXPECT_TRUE(web_contents_https->GetSiteInstance()->IsDefaultSiteInstance()); |
| EXPECT_NE(web_contents_http->GetSiteInstance(), |
| web_contents_https->GetSiteInstance()); |
| EXPECT_FALSE(web_contents_http->GetSiteInstance()->IsRelatedSiteInstance( |
| web_contents_https->GetSiteInstance())); |
| } |
| |
| EXPECT_NE(web_contents_http->GetSiteInstance()->GetProcess(), |
| web_contents_https->GetSiteInstance()->GetProcess()); |
| |
| EXPECT_EQ("", GetCookieFromJS(web_contents_https->GetPrimaryMainFrame())); |
| EXPECT_EQ("", GetCookieFromJS(web_contents_http->GetPrimaryMainFrame())); |
| |
| // Non-TLS page writes secure cookie. |
| EXPECT_TRUE(ExecJs(web_contents_http->GetPrimaryMainFrame(), |
| "document.cookie = 'A=1; secure;';")); |
| EXPECT_EQ("", GetCookieFromJS(web_contents_https->GetPrimaryMainFrame())); |
| EXPECT_EQ("", GetCookieFromJS(web_contents_http->GetPrimaryMainFrame())); |
| |
| // Non-TLS page writes not-secure cookie. |
| EXPECT_TRUE(ExecJs(web_contents_http->GetPrimaryMainFrame(), |
| "document.cookie = 'B=2';")); |
| EXPECT_EQ("B=2", GetCookieFromJS(web_contents_http->GetPrimaryMainFrame())); |
| EXPECT_EQ("B=2", GetCookieFromJS(web_contents_https->GetPrimaryMainFrame())); |
| |
| // TLS page writes secure cookie. |
| EXPECT_TRUE(ExecJs(web_contents_https->GetPrimaryMainFrame(), |
| "document.cookie = 'C=3;secure;';")); |
| EXPECT_EQ("B=2; C=3", |
| GetCookieFromJS(web_contents_https->GetPrimaryMainFrame())); |
| EXPECT_EQ("B=2", GetCookieFromJS(web_contents_http->GetPrimaryMainFrame())); |
| |
| // TLS page writes not-secure cookie. |
| EXPECT_TRUE(ExecJs(web_contents_https->GetPrimaryMainFrame(), |
| "document.cookie = 'D=4';")); |
| EXPECT_EQ("B=2; C=3; D=4", |
| GetCookieFromJS(web_contents_https->GetPrimaryMainFrame())); |
| EXPECT_EQ("B=2; D=4", |
| GetCookieFromJS(web_contents_http->GetPrimaryMainFrame())); |
| } |
| |
| // Ensure "priority" cookie option is settable via document.cookie. |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, CookiePriority) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| struct { |
| std::string param; |
| net::CookiePriority priority; |
| } cases[] = {{"name=value", net::COOKIE_PRIORITY_DEFAULT}, |
| {"name=value;priority=Low", net::COOKIE_PRIORITY_LOW}, |
| {"name=value;priority=Medium", net::COOKIE_PRIORITY_MEDIUM}, |
| {"name=value;priority=High", net::COOKIE_PRIORITY_HIGH}}; |
| |
| for (auto test_case : cases) { |
| GURL url = embedded_test_server()->GetURL("/set_document_cookie.html?" + |
| test_case.param); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| std::vector<net::CanonicalCookie> cookies = |
| GetCanonicalCookies(shell()->web_contents()->GetBrowserContext(), url); |
| |
| EXPECT_EQ(1u, cookies.size()); |
| EXPECT_EQ("name", cookies[0].Name()); |
| EXPECT_EQ("value", cookies[0].Value()); |
| EXPECT_EQ(test_case.priority, cookies[0].Priority()); |
| } |
| } |
| |
| // SameSite cookies (that aren't marked as http-only) should be available to |
| // JavaScript. |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, SameSiteCookies) { |
| // Must use HTTPS because SameSite=None cookies must be Secure. |
| |
| // The server sets eight cookies on 'a.test' and on 'b.test', then loads |
| // a page that frames both 'a.test' and 'b.test' under 'a.test'. |
| std::string cookies_to_set = |
| "/set-cookie?none=1;SameSite=None;Secure" // SameSite=None must be |
| // Secure. |
| "&none-insecure=1;SameSite=None" |
| "&strict=1;SameSite=Strict" |
| "&unspecified=1" // unspecified SameSite should be treated as Lax. |
| "&lax=1;SameSite=Lax" |
| "&none-http=1;SameSite=None;Secure;httponly" |
| "&strict-http=1;SameSite=Strict;httponly" |
| "&unspecified-http=1;httponly" |
| "&lax-http=1;SameSite=Lax;httponly"; |
| |
| GURL url = https_server_.GetURL("a.test", cookies_to_set); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| url = https_server_.GetURL("b.test", cookies_to_set); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| url = https_server_.GetURL( |
| "a.test", "/cross_site_iframe_factory.html?a.test(a.test(),b.test())"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame(); |
| RenderFrameHost* a_iframe = web_contents->GetPrimaryFrameTree() |
| .root() |
| ->child_at(0) |
| ->current_frame_host(); |
| RenderFrameHost* b_iframe = web_contents->GetPrimaryFrameTree() |
| .root() |
| ->child_at(1) |
| ->current_frame_host(); |
| |
| // The top-level frame should get all same-site cookies. |
| EXPECT_EQ("none=1; strict=1; unspecified=1; lax=1", |
| GetCookieFromJS(main_frame)); |
| |
| // Same-site cookies will be delievered to the 'a.com' frame, as it is same- |
| // site with its ancestors. |
| EXPECT_EQ("none=1; strict=1; unspecified=1; lax=1", |
| GetCookieFromJS(a_iframe)); |
| |
| // Same-site cookies should not be delievered to the 'b.com' frame, as it |
| // isn't same-site with its ancestors. The SameSite=None but insecure cookie |
| // is rejected. |
| EXPECT_EQ("none=1", GetCookieFromJS(b_iframe)); |
| } |
| |
| // Prefixed cookies (that aren't marked as http-only) should be available to |
| // JavaScript. |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, PrefixedCookies_Read) { |
| // Must use HTTPS because prefixed cookies must be Secure. |
| |
| ASSERT_TRUE(SetCookie(shell()->web_contents()->GetBrowserContext(), |
| https_server_.GetURL("a.test", "/"), |
| "__Host-cookie=1;Secure;Path=/")); |
| ASSERT_TRUE(SetCookie(shell()->web_contents()->GetBrowserContext(), |
| https_server_.GetURL("a.test", "/"), |
| "__Secure-cookie=1;Secure")); |
| ASSERT_TRUE(SetCookie(shell()->web_contents()->GetBrowserContext(), |
| https_server_.GetURL("a.test", "/"), |
| "__Secure-http-cookie=1;Secure;HttpOnly")); |
| |
| EXPECT_TRUE( |
| NavigateToURL(shell(), https_server_.GetURL("a.test", "/empty.html"))); |
| |
| EXPECT_THAT(EvalJs(shell(), "document.cookie").ExtractString(), |
| net::CookieStringIs(UnorderedElementsAre( |
| Key("__Host-cookie"), Key("__Secure-cookie")))); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, PrefixedCookies_Read_Insecure) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| ASSERT_TRUE(SetCookie(shell()->web_contents()->GetBrowserContext(), |
| https_server_.GetURL("a.test", "/"), |
| "__Host-cookie=1;Secure;Path=/")); |
| ASSERT_TRUE(SetCookie(shell()->web_contents()->GetBrowserContext(), |
| https_server_.GetURL("a.test", "/"), |
| "__Secure-cookie=1;Secure")); |
| ASSERT_TRUE(SetCookie(shell()->web_contents()->GetBrowserContext(), |
| https_server_.GetURL("a.test", "/"), |
| "__Secure-http-cookie=1;Secure;HttpOnly")); |
| |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.test", "/empty.html"))); |
| |
| EXPECT_EQ(EvalJs(shell(), "document.cookie"), ""); |
| } |
| |
| // Prefixed cookies should be writable by JavaScript. |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, PrefixedCookies_Write) { |
| // Must use HTTPS because prefixed cookies must be Secure. |
| |
| EXPECT_TRUE( |
| NavigateToURL(shell(), https_server_.GetURL("a.test", "/empty.html"))); |
| |
| EXPECT_TRUE(ExecJs(shell(), R"js( |
| // Valid cookies: |
| document.cookie = "__Host-cookie=1;Secure;Path=/"; |
| document.cookie = "__Secure-cookie=1;Secure"; |
| // Invalid cookies: |
| document.cookie = "__Secure-http-cookie=1;Secure;HttpOnly"; |
| document.cookie = "__Secure-missing-attr=1"; |
| document.cookie = "__Host-wrong-path=1;Secure;"; |
| document.cookie = "__Host-wrong-domain=1;Secure;Domain=a.test"; |
| document.cookie = "__Host-wrong-secure=1;Path=/"; |
| )js")); |
| |
| EXPECT_THAT(GetCanonicalCookies(shell()->web_contents()->GetBrowserContext(), |
| https_server_.GetURL("a.test", "/")), |
| UnorderedElementsAre( |
| net::MatchesCookieNameValue("__Host-cookie", "1"), |
| net::MatchesCookieNameValue("__Secure-cookie", "1"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, PrefixedCookies_Write_Insecure) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.test", "/empty.html"))); |
| |
| EXPECT_TRUE(ExecJs(shell(), R"js( |
| document.cookie = "__Host-cookie=1;Secure;Path=/"; |
| document.cookie = "__Secure-cookie=1;Secure"; |
| document.cookie = "__Secure-http-cookie=1;Secure;HttpOnly"; |
| document.cookie = "__Secure-missing-attr=1"; |
| document.cookie = "__Host-wrong-path=1;Secure;"; |
| document.cookie = "__Host-wrong-domain=1;Secure;Domain=a.test"; |
| document.cookie = "__Host-wrong-secure=1;Path=/"; |
| )js")); |
| |
| EXPECT_THAT( |
| GetCanonicalCookies(shell()->web_contents()->GetBrowserContext(), |
| embedded_test_server()->GetURL("a.test", "/")), |
| IsEmpty()); |
| } |
| |
| // embedded_test_server() uses http, which is insecure, but localhost is |
| // allowed to set prefixed cookies anyway. |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, PrefixedCookies_Write_Localhost) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("localhost", "/empty.html"))); |
| |
| EXPECT_TRUE(ExecJs(shell(), R"js( |
| document.cookie = "__Host-cookie=1;Secure;Path=/"; |
| document.cookie = "__Secure-cookie=1;Secure"; |
| document.cookie = "__Secure-http-cookie=1;Secure;HttpOnly"; |
| document.cookie = "__Secure-missing-attr=1"; |
| document.cookie = "__Host-wrong-path=1;Secure;"; |
| document.cookie = "__Host-wrong-domain=1;Secure;Domain=a.test"; |
| document.cookie = "__Host-wrong-secure=1;Path=/"; |
| )js")); |
| |
| EXPECT_THAT( |
| GetCanonicalCookies(shell()->web_contents()->GetBrowserContext(), |
| embedded_test_server()->GetURL("localhost", "/")), |
| UnorderedElementsAre( |
| net::MatchesCookieNameValue("__Host-cookie", "1"), |
| net::MatchesCookieNameValue("__Secure-cookie", "1"))); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, |
| CookieJarInvalidatesCacheWithNewDevtoolsControls) { |
| // Must use HTTPS because SameSite=None cookies must be Secure. |
| |
| // Set a single cookie that we'll access from a third-party context |
| std::string cookies_to_set = |
| "/set-cookie?none=1;SameSite=None;Secure"; // SameSite=None must be |
| // Secure |
| |
| GURL url = https_server_.GetURL("b.test", cookies_to_set); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| // Turn on third-party cookie restriction from devtools. This needs to happen |
| // from a top level client |
| TestDevToolsProtocolClient devtools_client; |
| devtools_client.AttachToWebContents(web_contents); |
| EnableDevtoolsThirdPartyCookieRestriction(devtools_client); |
| |
| url = https_server_.GetURL( |
| "a.test", "/cross_site_iframe_factory.html?a.test(b.test())"); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| RenderFrameHost* oop_iframe = web_contents->GetPrimaryFrameTree() |
| .root() |
| ->child_at(0) |
| ->current_frame_host(); |
| |
| // Attach devtools client to the sub frame, but disable the controls at first |
| devtools_client.DetachProtocolClient(); |
| devtools_client.AttachToFrameTreeHost(oop_iframe); |
| devtools_client.SendCommandSync("Network.disable"); |
| |
| // Check Get->Get |
| // Overrides should not apply after disabling the controls |
| EXPECT_EQ("none=1", GetCookieFromJS(oop_iframe)); |
| |
| // Confirm cache is invalidated by observing new value from document.cookie |
| // when re-enabling devtools |
| devtools_client.SendCommandSync("Network.enable"); |
| EXPECT_EQ("", GetCookieFromJS(oop_iframe)); |
| |
| // Check Set->Get |
| // Set a cookie with devtools disabled |
| devtools_client.SendCommandSync("Network.disable"); |
| SetCookieFromJS(oop_iframe, "none=2; SameSite=None; Secure"); |
| |
| // Confirm cache is invalidated by observing no cookie from document.cookie |
| // when re-enabling devtools |
| devtools_client.SendCommandSync("Network.enable"); |
| EXPECT_EQ("", GetCookieFromJS(oop_iframe)); |
| |
| devtools_client.DetachProtocolClient(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, CookieTruncatingCharFromJavascript) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| ASSERT_TRUE( |
| NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html"))); |
| |
| WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHost* frame = tab->GetPrimaryMainFrame(); |
| |
| // Test scenarios where a control char may appear at start, middle and end of |
| // a cookie line. Control char array with NULL (\x0), CR (\xD), and LF (xA). |
| const std::string kTestChars[] = {"\\x00", "\\x0D", "\\x0A"}; |
| |
| for (const std::string& ctl_string : kTestChars) { |
| // Control char at the start of the string. |
| // Note that when truncation of this cookie string occurs, no histogram |
| // entries get recorded because the code bails out early on the resulting |
| // empty cookie string. |
| std::string cookie_string = base::StrCat({ctl_string, "foo1=bar"}); |
| SetCookieFromJS(frame, cookie_string); |
| |
| // Control char in the middle of the string. |
| cookie_string = base::StrCat({"foo2=bar;", ctl_string, "httponly"}); |
| SetCookieFromJS(frame, cookie_string); |
| |
| cookie_string = base::StrCat({"foo3=ba", ctl_string, "r; httponly"}); |
| SetCookieFromJS(frame, cookie_string); |
| |
| // Control char at the end of the string. |
| cookie_string = base::StrCat({"foo4=bar;", ctl_string}); |
| SetCookieFromJS(frame, cookie_string); |
| } |
| |
| EXPECT_EQ("", GetCookieFromJS(frame)); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, CookieTruncatingCharFromHeaders) { |
| std::string cookie_string; |
| embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting( |
| [&](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->AddCustomHeader("Set-Cookie", cookie_string); |
| return std::move(response); |
| })); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL http_url = embedded_test_server()->GetURL("/"); |
| |
| // Test scenarios where a control char may appear at start, middle and end of |
| // a cookie line. Control char array with NULL (\x0), CR (\xD), and LF (xA) |
| char kTestChars[] = {'\x0', '\xD', '\xA'}; |
| |
| for (const auto& test : kTestChars) { |
| std::string ctl_string(1, test); |
| |
| // ctrl char at start of string |
| cookie_string = base::StrCat({ctl_string, "foo=bar"}); |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| // ctrl char at middle of string |
| cookie_string = base::StrCat({"foo=bar;", ctl_string, "httponly"}); |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| cookie_string = base::StrCat({"foo=ba", ctl_string, "r; httponly"}); |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| // ctrl char at end of string |
| cookie_string = base::StrCat({"foo=bar;", "httponly;", ctl_string}); |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| } |
| // Test if there are multiple control characters that terminate. |
| cookie_string = "foo=bar;\xA\xDhttponly"; |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| } |
| |
| class RestrictedCookieManagerInterceptor |
| : public network::mojom::RestrictedCookieManagerInterceptorForTesting { |
| public: |
| RestrictedCookieManagerInterceptor( |
| mojo::PendingReceiver<network::mojom::RestrictedCookieManager> receiver, |
| mojo::PendingRemote<network::mojom::RestrictedCookieManager> real_rcm) |
| : receiver_(this, std::move(receiver)), real_rcm_(std::move(real_rcm)) {} |
| |
| void set_override_url(std::optional<std::string> maybe_url) { |
| override_url_ = std::move(maybe_url); |
| } |
| |
| void SetCookieFromString( |
| const GURL& url, |
| const net::SiteForCookies& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| net::StorageAccessApiStatus storage_access_api_status, |
| bool get_version_shared_memory, |
| bool is_ad_tagged, |
| bool apply_devtools_overrides, |
| const std::string& cookie, |
| SetCookieFromStringCallback callback) override { |
| GetForwardingInterface()->SetCookieFromString( |
| URLToUse(url), site_for_cookies, top_frame_origin, |
| storage_access_api_status, get_version_shared_memory, is_ad_tagged, |
| apply_devtools_overrides, std::move(cookie), std::move(callback)); |
| } |
| |
| void GetCookiesString(const GURL& url, |
| const net::SiteForCookies& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| net::StorageAccessApiStatus storage_access_api_status, |
| bool get_version_shared_memory, |
| bool is_ad_tagged, |
| bool force_disable_third_party_cookies, |
| bool apply_devtools_overrides, |
| GetCookiesStringCallback callback) override { |
| GetForwardingInterface()->GetCookiesString( |
| URLToUse(url), site_for_cookies, top_frame_origin, |
| storage_access_api_status, get_version_shared_memory, is_ad_tagged, |
| force_disable_third_party_cookies, apply_devtools_overrides, |
| std::move(callback)); |
| } |
| |
| private: |
| network::mojom::RestrictedCookieManager* GetForwardingInterface() override { |
| return real_rcm_.get(); |
| } |
| |
| GURL URLToUse(const GURL& url_in) { |
| return override_url_ ? GURL(override_url_.value()) : url_in; |
| } |
| |
| std::optional<std::string> override_url_; |
| |
| mojo::Receiver<network::mojom::RestrictedCookieManager> receiver_; |
| mojo::Remote<network::mojom::RestrictedCookieManager> real_rcm_; |
| }; |
| |
| class CookieStoreContentBrowserClient |
| : public ContentBrowserTestContentBrowserClient { |
| public: |
| ~CookieStoreContentBrowserClient() override = default; |
| |
| bool WillCreateRestrictedCookieManager( |
| network::mojom::RestrictedCookieManagerRole role, |
| content::BrowserContext* browser_context, |
| const url::Origin& origin, |
| const net::IsolationInfo& isolation_info, |
| bool is_service_worker, |
| int process_id, |
| int routing_id, |
| mojo::PendingReceiver<network::mojom::RestrictedCookieManager>* receiver) |
| override { |
| mojo::PendingReceiver<network::mojom::RestrictedCookieManager> |
| orig_receiver = std::move(*receiver); |
| |
| mojo::PendingRemote<network::mojom::RestrictedCookieManager> real_rcm; |
| *receiver = real_rcm.InitWithNewPipeAndPassReceiver(); |
| |
| rcm_interceptor_ = std::make_unique<RestrictedCookieManagerInterceptor>( |
| std::move(orig_receiver), std::move(real_rcm)); |
| rcm_interceptor_->set_override_url(override_url_); |
| |
| return false; // only made a proxy, still need the actual impl to be made. |
| } |
| |
| void set_override_url(std::optional<std::string> maybe_url) { |
| override_url_ = maybe_url; |
| if (rcm_interceptor_) |
| rcm_interceptor_->set_override_url(override_url_); |
| } |
| |
| private: |
| std::optional<std::string> override_url_; |
| std::unique_ptr<RestrictedCookieManagerInterceptor> rcm_interceptor_; |
| }; |
| |
| // Cookie access in loader is locked to a particular origin, so messages |
| // for wrong URLs are rejected. |
| // TODO(crbug.com/41453892): This should actually result in renderer |
| // kills. |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, CrossSiteCookieSecurityEnforcement) { |
| // The code under test is only active under site isolation. |
| if (!AreAllSitesIsolatedForTesting()) { |
| return; |
| } |
| |
| SetupCrossSiteRedirector(embedded_test_server()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("/frame_with_load_event.html"))); |
| |
| WebContentsImpl* tab = static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| // The iframe on the http page should get its own process. |
| FrameTreeVisualizer v; |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site B ------- proxies for A\n" |
| "Where A = http://127.0.0.1/\n" |
| " B = http://baz.com/", |
| v.DepictFrameTree(tab->GetPrimaryFrameTree().root())); |
| |
| RenderFrameHost* main_frame = tab->GetPrimaryMainFrame(); |
| RenderFrameHost* iframe = |
| tab->GetPrimaryFrameTree().root()->child_at(0)->current_frame_host(); |
| |
| EXPECT_NE(iframe->GetProcess(), main_frame->GetProcess()); |
| |
| SetCookieDirect(tab, GURL("http://127.0.0.1/"), "A_cookie = parent"); |
| SetCookieDirect(tab, GURL("http://baz.com/"), "B_cookie = child"); |
| EXPECT_EQ("A_cookie=parent", |
| GetCookiesDirect(tab, GURL("http://127.0.0.1/"))); |
| |
| EXPECT_EQ("B_cookie=child", GetCookiesDirect(tab, GURL("http://baz.com/"))); |
| |
| // Try to get cross-site cookies from the subframe's process. |
| { |
| CookieStoreContentBrowserClient browser_client; |
| browser_client.set_override_url("http://127.0.0.1/"); |
| EXPECT_EQ("", GetCookieFromJS(iframe)); |
| } |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site B ------- proxies for A\n" |
| "Where A = http://127.0.0.1/\n" |
| " B = http://baz.com/", |
| v.DepictFrameTree(tab->GetPrimaryFrameTree().root())); |
| |
| // Now set a cross-site cookie from the main frame's process. |
| { |
| CookieStoreContentBrowserClient browser_client; |
| |
| browser_client.set_override_url("https://baz.com/"); |
| SetCookieFromJS(iframe, "pwn=ed"); |
| |
| EXPECT_EQ("B_cookie=child", GetCookiesDirect(tab, GURL("http://baz.com/"))); |
| } |
| |
| EXPECT_EQ( |
| " Site A ------------ proxies for B\n" |
| " +--Site B ------- proxies for A\n" |
| "Where A = http://127.0.0.1/\n" |
| " B = http://baz.com/", |
| v.DepictFrameTree(tab->GetPrimaryFrameTree().root())); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, CookieNotReadableAfterExpiry) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| GURL http_url = embedded_test_server()->GetURL("example.test", "/empty.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| WebContentsImpl* web_contents_http = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHost* frame = web_contents_http->GetPrimaryMainFrame(); |
| |
| SetCookieFromJS(frame, "c=1;Max-Age=1"); |
| SetCookieFromJS(frame, "d=1;Max-Age=7200"); |
| EXPECT_EQ("c=1; d=1", GetCookieFromJS(frame)); |
| |
| // If cookies properly expire and become unavailable this test will terminate. |
| // If they do not the test will time out. The earliest expiry from the cookies |
| // is used so the short expiry from c is expected to be used. |
| std::string cookie; |
| do { |
| cookie = GetCookieFromJS(frame); |
| base::PlatformThread::Sleep(base::Milliseconds(100)); |
| } while (cookie != "d=1"); |
| } |
| |
| // Cookies for an eTLD should be stored (via JS) if they match the URL host, |
| // even if they begin with `.` or have non-canonical capitalization. |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, ETldDomainCookies) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // This test uses `gov.br` as an example of an eTLD. |
| GURL http_url = embedded_test_server()->GetURL("gov.br", "/empty.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| WebContentsImpl* web_contents_http = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHost* frame = web_contents_http->GetPrimaryMainFrame(); |
| |
| const char* kCases[] = { |
| // A host cookie. |
| "c=1", |
| // A cookie for this domain. |
| "c=1; domain=gov.br", |
| // Same, but with a preceding dot. This dot should be ignored. |
| "c=1; domain=.gov.br", |
| // Same, but with non-canonical case. This should be canonicalized. |
| "c=1; domain=gOv.bR", |
| }; |
| |
| for (const char* set_cookie : kCases) { |
| SCOPED_TRACE(set_cookie); |
| SetCookieFromJS(frame, set_cookie); |
| EXPECT_EQ("c=1", GetCookieFromJS(frame)); |
| SetCookieFromJS(frame, "c=;expires=Thu, 01 Jan 1970 00:00:00 GMT"); |
| EXPECT_EQ("", GetCookieFromJS(frame)); |
| } |
| } |
| |
| // Cookies for an eTLD should be stored (via header) if they match the URL host, |
| // even if they begin with `.` or have non-canonical capitalization. |
| IN_PROC_BROWSER_TEST_P(CookieBrowserTest, ETldDomainCookiesHeader) { |
| std::string got_cookie_on_request; |
| std::string set_cookie_on_response; |
| embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting( |
| [&](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| if (request.headers.contains("Cookie")) { |
| got_cookie_on_request = request.headers.at("Cookie"); |
| } else { |
| got_cookie_on_request = ""; |
| } |
| if (set_cookie_on_response.size() != 0) { |
| response->AddCustomHeader("Set-Cookie", set_cookie_on_response); |
| set_cookie_on_response = ""; |
| } |
| return std::move(response); |
| })); |
| |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| // This test uses `gov.br` as an example of an eTLD. |
| GURL http_url = embedded_test_server()->GetURL("gov.br", "/empty.html"); |
| |
| const char* kCases[] = { |
| // A host cookie. |
| "c=1", |
| // A cookie for this domain. |
| "c=1; domain=gov.br", |
| // Same, but with a preceding dot. This dot should be ignored. |
| "c=1; domain=.gov.br", |
| // Same, but with non-canonical case. This should be canonicalized. |
| "c=1; domain=gOv.bR", |
| }; |
| |
| for (const char* set_cookie : kCases) { |
| SCOPED_TRACE(set_cookie); |
| |
| set_cookie_on_response = set_cookie; |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| EXPECT_EQ("c=1", got_cookie_on_request); |
| |
| set_cookie_on_response = "c=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), http_url)); |
| EXPECT_EQ("", got_cookie_on_request); |
| } |
| } |
| |
| enum class CookieFileMode { kDefault, kEnabled, kDisabled }; |
| |
| class CookieFileBrowserTest |
| : public ContentBrowserTest, |
| public ::testing::WithParamInterface<CookieFileMode> { |
| protected: |
| void SetUpOnMainThread() override { |
| // Setup file url. |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| EXPECT_TRUE(file_directory_.CreateUniqueTempDir()); |
| base::FilePath file_path = |
| file_directory_.GetPath().AppendASCII("index.html"); |
| EXPECT_TRUE(base::WriteFile(file_path, "")); |
| file_url_ = net::FilePathToFileURL(file_path); |
| |
| // Setup cookie manager. |
| bool file_cookie_enabled; |
| switch (GetParam()) { |
| case CookieFileMode::kDefault: |
| // Nothing to do. |
| return; |
| case CookieFileMode::kEnabled: |
| file_cookie_enabled = true; |
| break; |
| case CookieFileMode::kDisabled: |
| file_cookie_enabled = false; |
| break; |
| } |
| base::RunLoop run_loop; |
| shell() |
| ->web_contents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetCookieManagerForBrowserProcess() |
| ->AllowFileSchemeCookies(file_cookie_enabled, |
| base::BindLambdaForTesting([&](bool success) { |
| EXPECT_TRUE(success); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| GURL file_url_; |
| |
| private: |
| base::ScopedTempDir file_directory_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(, |
| CookieFileBrowserTest, |
| ::testing::Values(CookieFileMode::kDefault, |
| CookieFileMode::kEnabled, |
| CookieFileMode::kDisabled)); |
| |
| // Try to set and get cookies on a file URL. |
| IN_PROC_BROWSER_TEST_P(CookieFileBrowserTest, SetAndGetCookie) { |
| // Navigate to file. |
| EXPECT_TRUE(NavigateToURL(shell(), file_url_)); |
| RenderFrameHost* frame = shell()->web_contents()->GetPrimaryMainFrame(); |
| |
| // File cookies always appear to be writable. On non-Android platforms a |
| // warning is printed when this occurs. |
| #if !BUILDFLAG(IS_ANDROID) |
| WebContentsConsoleObserver console_observer(shell()->web_contents()); |
| console_observer.SetPattern( |
| "While navigator.cookieEnabled does return true for this file:// " |
| "URL, this is done for web compatability reasons. Cookies will not " |
| "actually be stored for file:// URLs. If you want this to change " |
| "please leave feedback on crbug.com/378604901."); |
| #endif |
| EXPECT_TRUE(EvalJs(frame, "navigator.cookieEnabled").ExtractBool()); |
| #if !BUILDFLAG(IS_ANDROID) |
| ASSERT_TRUE(console_observer.Wait()); |
| #endif |
| |
| // File cookies can only be set if they are enabled. |
| bool can_set_cookies; |
| switch (GetParam()) { |
| case CookieFileMode::kDefault: |
| // TODO(crbug.com/378604901): Perhapse this should be allowed by default. |
| can_set_cookies = false; |
| return; |
| case CookieFileMode::kEnabled: |
| can_set_cookies = true; |
| break; |
| case CookieFileMode::kDisabled: |
| can_set_cookies = false; |
| break; |
| } |
| SetCookieFromJS(frame, "test=1"); |
| EXPECT_EQ(can_set_cookies ? "test=1" : "", GetCookieFromJS(frame)); |
| } |
| |
| } // namespace content |