1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "extensions/browser/extension_navigation_throttle.h"
6 
7 #include <memory>
8 #include "base/strings/stringprintf.h"
9 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
10 #include "components/crx_file/id_util.h"
11 #include "content/public/browser/content_browser_client.h"
12 #include "content/public/browser/navigation_handle.h"
13 #include "content/public/common/content_client.h"
14 #include "content/public/test/mock_navigation_handle.h"
15 #include "content/public/test/navigation_simulator.h"
16 #include "content/public/test/test_renderer_host.h"
17 #include "content/public/test/web_contents_tester.h"
18 #include "extensions/browser/extension_registry.h"
19 #include "extensions/common/extension.h"
20 #include "extensions/common/extension_builder.h"
21 #include "extensions/common/identifiability_metrics.h"
22 #include "extensions/common/value_builder.h"
23 #include "services/metrics/public/cpp/ukm_source_id.h"
24 #include "testing/gtest/include/gtest/gtest.h"
25 #include "third_party/blink/public/common/privacy_budget/identifiability_metrics.h"
26 #include "third_party/blink/public/common/privacy_budget/identifiable_surface.h"
27 #include "third_party/blink/public/common/privacy_budget/scoped_identifiability_test_sample_collector.h"
28 #include "url/gurl.h"
29 
30 using content::NavigationThrottle;
31 
32 namespace extensions {
33 
34 namespace {
35 
36 const char kAccessible[] = "accessible.html";
37 const char kPrivate[] = "private.html";
38 const char kAccessibleDir[] = "accessible_dir/*";
39 const char kAccessibleDirResource[] = "accessible_dir/foo.html";
40 
41 class MockBrowserClient : public content::ContentBrowserClient {
42  public:
MockBrowserClient()43   MockBrowserClient() {}
~MockBrowserClient()44   ~MockBrowserClient() override {}
45 
46   // Only construct an ExtensionNavigationThrottle so that we can test it in
47   // isolation.
CreateThrottlesForNavigation(content::NavigationHandle * handle)48   std::vector<std::unique_ptr<NavigationThrottle>> CreateThrottlesForNavigation(
49       content::NavigationHandle* handle) override {
50     std::vector<std::unique_ptr<NavigationThrottle>> throttles;
51     throttles.push_back(std::make_unique<ExtensionNavigationThrottle>(handle));
52     return throttles;
53   }
54 };
55 
56 }  // namespace
57 
58 class ExtensionNavigationThrottleUnitTest
59     : public ChromeRenderViewHostTestHarness {
60  public:
ExtensionNavigationThrottleUnitTest()61   ExtensionNavigationThrottleUnitTest() {}
SetUp()62   void SetUp() override {
63     ChromeRenderViewHostTestHarness::SetUp();
64     original_client_ = content::SetBrowserClientForTesting(&client_);
65     AddExtension();
66   }
67 
TearDown()68   void TearDown() override {
69     content::SetBrowserClientForTesting(original_client_);
70     ChromeRenderViewHostTestHarness::TearDown();
71   }
72 
73   // Checks that trying to navigate the given |host| to |extension_url| results
74   // in the |expected_will_start_result|, and also that navigating to
75   // |extension_url| via http redirect gives the same result.
CheckTestCase(content::RenderFrameHost * host,const GURL & extension_url,NavigationThrottle::ThrottleAction expected_will_start_result)76   void CheckTestCase(
77       content::RenderFrameHost* host,
78       const GURL& extension_url,
79       NavigationThrottle::ThrottleAction expected_will_start_result) {
80     // First subtest: direct navigation to |extension_url|.
81     content::MockNavigationHandle test_handle(extension_url, host);
82     test_handle.set_initiator_origin(host->GetLastCommittedOrigin());
83     test_handle.set_starting_site_instance(host->GetSiteInstance());
84     auto throttle = std::make_unique<ExtensionNavigationThrottle>(&test_handle);
85 
86     {
87       blink::test::ScopedIdentifiabilityTestSampleCollector metrics;
88 
89       EXPECT_EQ(expected_will_start_result,
90                 throttle->WillStartRequest().action())
91           << extension_url;
92 
93       ExpectExtensionAccessResult(expected_will_start_result, extension_url,
94                                   test_handle.GetNavigationId(),
95                                   metrics.entries());
96     }
97 
98     // Second subtest: server redirect to
99     // |extension_url|.
100     {
101       blink::test::ScopedIdentifiabilityTestSampleCollector metrics;
102 
103       GURL http_url("https://example.com");
104       test_handle.set_url(http_url);
105 
106       EXPECT_EQ(NavigationThrottle::PROCEED,
107                 throttle->WillStartRequest().action())
108           << http_url;
109       EXPECT_EQ(0u, metrics.entries().size());
110 
111       test_handle.set_url(extension_url);
112       EXPECT_EQ(expected_will_start_result,
113                 throttle->WillRedirectRequest().action())
114           << extension_url;
115       ExpectExtensionAccessResult(expected_will_start_result, extension_url,
116                                   test_handle.GetNavigationId(),
117                                   metrics.entries());
118     }
119   }
120 
extension()121   const Extension* extension() { return extension_.get(); }
web_contents_tester()122   content::WebContentsTester* web_contents_tester() {
123     return content::WebContentsTester::For(web_contents());
124   }
render_frame_host_tester(content::RenderFrameHost * host)125   content::RenderFrameHostTester* render_frame_host_tester(
126       content::RenderFrameHost* host) {
127     return content::RenderFrameHostTester::For(host);
128   }
129 
ExpectExtensionAccessResult(NavigationThrottle::ThrottleAction expected_action,const GURL & extension_url,int64_t navigation_id,const std::vector<blink::test::ScopedIdentifiabilityTestSampleCollector::Entry> & entries)130   void ExpectExtensionAccessResult(
131       NavigationThrottle::ThrottleAction expected_action,
132       const GURL& extension_url,
133       int64_t navigation_id,
134       const std::vector<
135           blink::test::ScopedIdentifiabilityTestSampleCollector::Entry>&
136           entries) {
137     // If throttle doesn't intervene, recording will be done by
138     // ExtensionURLLoaderFactory, not the throttle.
139     if (expected_action == NavigationThrottle::PROCEED) {
140       EXPECT_EQ(0u, entries.size());
141       return;
142     }
143 
144     ExtensionResourceAccessResult expected;
145     if (expected_action == NavigationThrottle::BLOCK_REQUEST) {
146       expected = ExtensionResourceAccessResult::kFailure;
147     } else if (expected_action == NavigationThrottle::CANCEL) {
148       expected = ExtensionResourceAccessResult::kCancel;
149     } else {
150       ADD_FAILURE() << "Unhandled action:" << expected_action;
151       return;
152     }
153 
154     ukm::SourceId source_id = ukm::ConvertToSourceId(
155         navigation_id, ukm::SourceIdObj::Type::NAVIGATION_ID);
156 
157     ASSERT_EQ(1u, entries.size());
158     EXPECT_EQ(source_id, entries[0].source);
159     ASSERT_EQ(1u, entries[0].metrics.size());
160     EXPECT_EQ(blink::IdentifiableSurface::FromTypeAndToken(
161                   blink::IdentifiableSurface::Type::kExtensionFileAccess,
162                   base::as_bytes(base::make_span(
163                       ExtensionSet::GetExtensionIdByURL(extension_url)))),
164               entries[0].metrics[0].surface);
165     EXPECT_EQ(blink::IdentifiableToken(expected), entries[0].metrics[0].value);
166   }
167 
168  private:
169   // Constructs an extension with accessible.html and accessible_dir/* as
170   // accessible resources.
AddExtension()171   void AddExtension() {
172     DictionaryBuilder manifest;
173     manifest.Set("name", "ext")
174         .Set("description", "something")
175         .Set("version", "0.1")
176         .Set("manifest_version", 2)
177         .Set("web_accessible_resources",
178              ListBuilder().Append(kAccessible).Append(kAccessibleDir).Build());
179     extension_ = ExtensionBuilder()
180                      .SetManifest(manifest.Build())
181                      .SetID(crx_file::id_util::GenerateId("foo"))
182                      .Build();
183     ASSERT_TRUE(extension_);
184     ExtensionRegistry::Get(browser_context())->AddEnabled(extension_.get());
185   }
186 
187   scoped_refptr<const Extension> extension_;
188   MockBrowserClient client_;
189   content::ContentBrowserClient* original_client_;
190 
191   DISALLOW_COPY_AND_ASSIGN(ExtensionNavigationThrottleUnitTest);
192 };
193 
194 // Tests the basic case of an external web page embedding an extension resource.
TEST_F(ExtensionNavigationThrottleUnitTest,ExternalWebPage)195 TEST_F(ExtensionNavigationThrottleUnitTest, ExternalWebPage) {
196   web_contents_tester()->NavigateAndCommit(GURL("http://example.com"));
197   content::RenderFrameHost* child =
198       render_frame_host_tester(main_rfh())->AppendChild("child");
199 
200   // Only resources specified in web_accessible_resources should be allowed.
201   CheckTestCase(child, extension()->GetResourceURL(kPrivate),
202                 NavigationThrottle::BLOCK_REQUEST);
203   CheckTestCase(child, extension()->GetResourceURL(kAccessible),
204                 NavigationThrottle::PROCEED);
205   CheckTestCase(child, extension()->GetResourceURL(kAccessibleDirResource),
206                 NavigationThrottle::PROCEED);
207 }
208 
TEST_F(ExtensionNavigationThrottleUnitTest,CrossSiteFileSystemUrl)209 TEST_F(ExtensionNavigationThrottleUnitTest, CrossSiteFileSystemUrl) {
210   web_contents_tester()->NavigateAndCommit(GURL("http://example.com"));
211 
212   GURL access_filesystem(base::StringPrintf(
213       "filesystem:%s/",
214       extension()->GetResourceURL(kAccessible).spec().c_str()));
215   CheckTestCase(main_rfh(), access_filesystem, NavigationThrottle::CANCEL);
216 }
217 
218 // Tests that the owning extension can access any of its resources.
TEST_F(ExtensionNavigationThrottleUnitTest,SameExtension)219 TEST_F(ExtensionNavigationThrottleUnitTest, SameExtension) {
220   web_contents_tester()->NavigateAndCommit(
221       extension()->GetResourceURL("trusted.html"));
222   content::RenderFrameHost* child =
223       render_frame_host_tester(main_rfh())->AppendChild("child");
224 
225   // All resources should be allowed.
226   CheckTestCase(child, extension()->GetResourceURL(kPrivate),
227                 NavigationThrottle::PROCEED);
228   CheckTestCase(child, extension()->GetResourceURL(kAccessible),
229                 NavigationThrottle::PROCEED);
230   CheckTestCase(child, extension()->GetResourceURL(kAccessibleDirResource),
231                 NavigationThrottle::PROCEED);
232 }
233 
234 // Tests that requests to disabled or non-existent extensions are blocked.
TEST_F(ExtensionNavigationThrottleUnitTest,DisabledExtensionChildFrame)235 TEST_F(ExtensionNavigationThrottleUnitTest, DisabledExtensionChildFrame) {
236   web_contents_tester()->NavigateAndCommit(GURL("http://example.com"));
237   content::RenderFrameHost* child =
238       render_frame_host_tester(main_rfh())->AppendChild("child");
239 
240   ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context());
241   registry->RemoveEnabled(extension()->id());
242   registry->AddDisabled(extension());
243 
244   // Since the extension is disabled, all requests should be blocked.
245   CheckTestCase(child, extension()->GetResourceURL(kPrivate),
246                 NavigationThrottle::BLOCK_REQUEST);
247   CheckTestCase(child, extension()->GetResourceURL(kAccessible),
248                 NavigationThrottle::BLOCK_REQUEST);
249   CheckTestCase(child, extension()->GetResourceURL(kAccessibleDirResource),
250                 NavigationThrottle::BLOCK_REQUEST);
251 
252   std::string second_id = crx_file::id_util::GenerateId("bar");
253   ASSERT_NE(second_id, extension()->id());
254   GURL unknown_url(base::StringPrintf("chrome-extension://%s/accessible.html",
255                                       second_id.c_str()));
256   // Requests to non-existent extensions should be blocked.
257   CheckTestCase(child, unknown_url, NavigationThrottle::BLOCK_REQUEST);
258 
259   // Test blob and filesystem URLs with disabled/unknown extensions.
260   GURL disabled_blob(base::StringPrintf("blob:chrome-extension://%s/SOMEGUID",
261                                         extension()->id().c_str()));
262   GURL unknown_blob(base::StringPrintf("blob:chrome-extension://%s/SOMEGUID",
263                                        second_id.c_str()));
264   CheckTestCase(child, disabled_blob, NavigationThrottle::BLOCK_REQUEST);
265   CheckTestCase(child, unknown_blob, NavigationThrottle::BLOCK_REQUEST);
266   GURL disabled_filesystem(
267       base::StringPrintf("filesystem:chrome-extension://%s/temporary/foo.html",
268                          extension()->id().c_str()));
269   GURL unknown_filesystem(
270       base::StringPrintf("filesystem:chrome-extension://%s/temporary/foo.html",
271                          second_id.c_str()));
272   CheckTestCase(child, disabled_filesystem, NavigationThrottle::BLOCK_REQUEST);
273   CheckTestCase(child, unknown_filesystem, NavigationThrottle::BLOCK_REQUEST);
274 }
275 
276 // Tests that requests to disabled or non-existent extensions are blocked.
TEST_F(ExtensionNavigationThrottleUnitTest,DisabledExtensionMainFrame)277 TEST_F(ExtensionNavigationThrottleUnitTest, DisabledExtensionMainFrame) {
278   web_contents_tester()->NavigateAndCommit(GURL("http://example.com"));
279 
280   ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context());
281   registry->RemoveEnabled(extension()->id());
282   registry->AddDisabled(extension());
283 
284   // Since the extension is disabled, all requests should be blocked.
285   CheckTestCase(main_rfh(), extension()->GetResourceURL(kPrivate),
286                 NavigationThrottle::BLOCK_REQUEST);
287   CheckTestCase(main_rfh(), extension()->GetResourceURL(kAccessible),
288                 NavigationThrottle::BLOCK_REQUEST);
289   CheckTestCase(main_rfh(), extension()->GetResourceURL(kAccessibleDirResource),
290                 NavigationThrottle::BLOCK_REQUEST);
291 
292   std::string second_id = crx_file::id_util::GenerateId("bar");
293 
294   ASSERT_NE(second_id, extension()->id());
295   GURL unknown_url(base::StringPrintf("chrome-extension://%s/accessible.html",
296                                       second_id.c_str()));
297   // Requests to non-existent extensions should be blocked.
298   CheckTestCase(main_rfh(), unknown_url, NavigationThrottle::BLOCK_REQUEST);
299 
300   // Test blob and filesystem URLs with disabled/unknown extensions.
301   GURL disabled_blob(base::StringPrintf("blob:chrome-extension://%s/SOMEGUID",
302                                         extension()->id().c_str()));
303   GURL unknown_blob(base::StringPrintf("blob:chrome-extension://%s/SOMEGUID",
304                                        second_id.c_str()));
305   CheckTestCase(main_rfh(), disabled_blob, NavigationThrottle::BLOCK_REQUEST);
306   CheckTestCase(main_rfh(), unknown_blob, NavigationThrottle::BLOCK_REQUEST);
307   GURL disabled_filesystem(
308       base::StringPrintf("filesystem:chrome-extension://%s/temporary/foo.html",
309                          extension()->id().c_str()));
310   GURL unknown_filesystem(
311       base::StringPrintf("filesystem:chrome-extension://%s/temporary/foo.html",
312                          second_id.c_str()));
313   CheckTestCase(main_rfh(), disabled_filesystem,
314                 NavigationThrottle::BLOCK_REQUEST);
315   CheckTestCase(main_rfh(), unknown_filesystem,
316                 NavigationThrottle::BLOCK_REQUEST);
317 }
318 
319 }  // namespace extensions
320