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