blob: 6acb19d0c97657f118d915d53ac0bcd2b3b32eda [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/api/web_request/web_request_permissions.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/stringprintf.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/api/web_request/web_request_info.h"
#include "extensions/browser/info_map.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/permissions/permission_set.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/common/url_pattern.h"
#include "extensions/common/url_pattern_set.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace extensions {
TEST(ExtensionWebRequestPermissions, TestHideRequestForURL) {
enum HideRequestMask {
HIDE_NONE = 0,
HIDE_RENDERER_REQUEST = 1,
HIDE_SUB_FRAME_NAVIGATION = 2,
HIDE_MAIN_FRAME_NAVIGATION = 4,
HIDE_BROWSER_SUB_RESOURCE_REQUEST = 8,
HIDE_ALL = HIDE_RENDERER_REQUEST | HIDE_SUB_FRAME_NAVIGATION |
HIDE_MAIN_FRAME_NAVIGATION | HIDE_BROWSER_SUB_RESOURCE_REQUEST,
};
ExtensionsAPIClient api_client;
auto info_map = base::MakeRefCounted<extensions::InfoMap>();
struct TestCase {
const char* url;
int expected_hide_request_mask;
} cases[] = {
{"https://www.google.com", HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"http://www.example.com", HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"https://www.example.com", HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"https://clients.google.com",
HIDE_BROWSER_SUB_RESOURCE_REQUEST | HIDE_SUB_FRAME_NAVIGATION},
{"http://clients4.google.com",
HIDE_BROWSER_SUB_RESOURCE_REQUEST | HIDE_SUB_FRAME_NAVIGATION},
{"https://clients4.google.com",
HIDE_BROWSER_SUB_RESOURCE_REQUEST | HIDE_SUB_FRAME_NAVIGATION},
{"https://clients9999.google.com",
HIDE_BROWSER_SUB_RESOURCE_REQUEST | HIDE_SUB_FRAME_NAVIGATION},
{"https://clients9999..google.com", HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"https://clients9999.example.google.com",
HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"https://clients.google.com.",
HIDE_BROWSER_SUB_RESOURCE_REQUEST | HIDE_SUB_FRAME_NAVIGATION},
{"https://.clients.google.com.",
HIDE_BROWSER_SUB_RESOURCE_REQUEST | HIDE_SUB_FRAME_NAVIGATION},
{"https://test.clients.google.com",
HIDE_SUB_FRAME_NAVIGATION | HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"http://google.example.com", HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"http://www.example.com", HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"https://www.example.com", HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"https://sb-ssl.google.com", HIDE_ALL},
{"https://sb-ssl.random.google.com", HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"https://safebrowsing.googleapis.com", HIDE_ALL},
// Unsupported scheme.
{"blob:https://safebrowsing.googleapis.com/"
"fc3f440b-78ed-469f-8af8-7a1717ff39ae",
HIDE_ALL},
{"filesystem:https://safebrowsing.googleapis.com/path", HIDE_ALL},
{"https://safebrowsing.googleapis.com.", HIDE_ALL},
{"https://safebrowsing.googleapis.com/v4", HIDE_ALL},
{"https://safebrowsing.googleapis.com:80/v4", HIDE_ALL},
{"https://safebrowsing.googleapis.com./v4", HIDE_ALL},
{"https://safebrowsing.googleapis.com/v5", HIDE_ALL},
{"https://safebrowsing.google.com/safebrowsing", HIDE_ALL},
{"https://safebrowsing.google.com/safebrowsing/anything", HIDE_ALL},
{"https://safebrowsing.google.com", HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"https://chrome.google.com", HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"http://www.google.com/", HIDE_BROWSER_SUB_RESOURCE_REQUEST},
{"https://chrome.google.com/webstore", HIDE_ALL},
{"https://chrome.google.com./webstore", HIDE_ALL},
{"https://chrome.google.com./webstore/", HIDE_ALL},
// Unsupported scheme.
{"blob:https://chrome.google.com/fc3f440b-78ed-469f-8af8-7a1717ff39ae",
HIDE_ALL},
{"notregisteredscheme://www.foobar.com", HIDE_ALL},
{"https://chrome.google.com:80/webstore", HIDE_ALL},
{"https://chrome.google.com/webstore?query", HIDE_ALL},
{"http://clients2.google.com/service/update2/crx", HIDE_ALL},
{"https://clients2.google.com/service/update2/crx", HIDE_ALL},
{"https://chrome.google.com/webstore/inlineinstall/detail/"
"kcnhkahnjcbndmmehfkdnkjomaanaooo",
HIDE_ALL},
};
const int kRendererProcessId = 1;
const int kBrowserProcessId = -1;
// Returns a WebRequestInfo instance constructed as per the given parameters.
auto create_request = [](const GURL& url, content::ResourceType type,
int render_process_id) {
WebRequestInfo request;
request.url = url;
request.type = type;
request.render_process_id = render_process_id;
request.web_request_type = ToWebRequestResourceType(type);
request.is_browser_side_navigation =
type == content::RESOURCE_TYPE_MAIN_FRAME ||
type == content::RESOURCE_TYPE_SUB_FRAME;
return request;
};
for (const TestCase& test_case : cases) {
SCOPED_TRACE(test_case.url);
GURL request_url(test_case.url);
ASSERT_TRUE(request_url.is_valid());
{
SCOPED_TRACE("Renderer initiated sub-resource request");
WebRequestInfo request = create_request(
request_url, content::RESOURCE_TYPE_SUB_RESOURCE, kRendererProcessId);
bool expect_hidden =
test_case.expected_hide_request_mask & HIDE_RENDERER_REQUEST;
EXPECT_EQ(expect_hidden,
WebRequestPermissions::HideRequest(info_map.get(), request));
}
{
SCOPED_TRACE("Browser initiated sub-resource request");
WebRequestInfo request = create_request(
request_url, content::RESOURCE_TYPE_SUB_RESOURCE, kBrowserProcessId);
bool expect_hidden = test_case.expected_hide_request_mask &
HIDE_BROWSER_SUB_RESOURCE_REQUEST;
EXPECT_EQ(expect_hidden,
WebRequestPermissions::HideRequest(info_map.get(), request));
}
{
SCOPED_TRACE("Main-frame navigation");
WebRequestInfo request = create_request(
request_url, content::RESOURCE_TYPE_MAIN_FRAME, kBrowserProcessId);
bool expect_hidden =
test_case.expected_hide_request_mask & HIDE_MAIN_FRAME_NAVIGATION;
EXPECT_EQ(expect_hidden,
WebRequestPermissions::HideRequest(info_map.get(), request));
}
{
SCOPED_TRACE("Sub-frame navigation");
WebRequestInfo request = create_request(
request_url, content::RESOURCE_TYPE_SUB_FRAME, kBrowserProcessId);
bool expect_hidden =
test_case.expected_hide_request_mask & HIDE_SUB_FRAME_NAVIGATION;
EXPECT_EQ(expect_hidden,
WebRequestPermissions::HideRequest(info_map.get(), request));
}
}
// Check protection of requests originating from the frame showing the Chrome
// WebStore. Normally this request is not protected:
GURL non_sensitive_url("http://www.google.com/test.js");
WebRequestInfo non_sensitive_request_info = create_request(
non_sensitive_url, content::RESOURCE_TYPE_SCRIPT, kRendererProcessId);
EXPECT_FALSE(WebRequestPermissions::HideRequest(info_map.get(),
non_sensitive_request_info));
// If the origin is labeled by the WebStoreAppId, it becomes protected.
{
const int kWebstoreProcessId = 42;
const int kSiteInstanceId = 23;
info_map->RegisterExtensionProcess(extensions::kWebStoreAppId,
kWebstoreProcessId, kSiteInstanceId);
WebRequestInfo sensitive_request_info = create_request(
non_sensitive_url, content::RESOURCE_TYPE_SCRIPT, kWebstoreProcessId);
EXPECT_TRUE(WebRequestPermissions::HideRequest(info_map.get(),
sensitive_request_info));
}
// Check that a request for a non-sensitive URL is rejected if it's a PAC
// script fetch.
non_sensitive_request_info.is_pac_request = true;
EXPECT_TRUE(WebRequestPermissions::HideRequest(info_map.get(),
non_sensitive_request_info));
}
TEST(ExtensionWebRequestPermissions,
CanExtensionAccessURLWithWithheldPermissions) {
// The InfoMap requires methods to be called on the IO thread. Fake it.
content::TestBrowserThreadBundle thread_bundle(
content::TestBrowserThreadBundle::IO_MAINLOOP);
scoped_refptr<const Extension> extension =
ExtensionBuilder("ext").AddPermission("<all_urls>").Build();
URLPatternSet all_urls(
{URLPattern(Extension::kValidHostPermissionSchemes, "<all_urls>")});
// Simulate withholding the <all_urls> permission.
extension->permissions_data()->SetPermissions(
std::make_unique<PermissionSet>(), // active permissions.
std::make_unique<PermissionSet>(
APIPermissionSet(), ManifestPermissionSet(), all_urls,
URLPatternSet()) /* withheld permissions */);
scoped_refptr<InfoMap> info_map = base::MakeRefCounted<InfoMap>();
info_map->AddExtension(extension.get(), base::Time(), false, false);
auto get_access = [extension, info_map](
const GURL& url,
const base::Optional<url::Origin>& initiator,
const base::Optional<content::ResourceType>&
resource_type) {
constexpr int kTabId = 42;
constexpr WebRequestPermissions::HostPermissionsCheck kPermissionsCheck =
WebRequestPermissions::REQUIRE_HOST_PERMISSION_FOR_URL;
return WebRequestPermissions::CanExtensionAccessURL(
info_map.get(), extension->id(), url, kTabId,
false /* crosses incognito */, kPermissionsCheck, initiator,
resource_type);
};
const GURL example_com("https://example.com");
const GURL chromium_org("https://chromium.org");
const url::Origin example_com_origin(url::Origin::Create(example_com));
const url::Origin chromium_org_origin(url::Origin::Create(chromium_org));
GURL urls[] = {example_com, chromium_org};
base::Optional<url::Origin> initiators[] = {base::nullopt, example_com_origin,
chromium_org_origin};
base::Optional<content::ResourceType> resource_types[] = {
base::nullopt, content::RESOURCE_TYPE_SUB_RESOURCE,
content::RESOURCE_TYPE_MAIN_FRAME};
// With all permissions withheld, the result of any request should be
// kWithheld.
for (const auto& url : urls) {
for (const auto& initiator : initiators) {
for (const auto& resource_type : resource_types) {
EXPECT_EQ(PermissionsData::PageAccess::kWithheld,
get_access(url, initiator, resource_type));
}
}
}
// Grant access to chromium.org.
URLPatternSet chromium_org_patterns({URLPattern(
Extension::kValidHostPermissionSchemes, "https://chromium.org/*")});
extension->permissions_data()->SetPermissions(
std::make_unique<PermissionSet>(APIPermissionSet(),
ManifestPermissionSet(),
chromium_org_patterns, URLPatternSet()),
std::make_unique<PermissionSet>(APIPermissionSet(),
ManifestPermissionSet(), all_urls,
URLPatternSet()));
// example.com isn't granted, so without an initiator or with an initiator
// that the extension doesn't have access to, access is withheld.
EXPECT_EQ(PermissionsData::PageAccess::kWithheld,
get_access(example_com, base::nullopt,
content::RESOURCE_TYPE_SUB_RESOURCE));
EXPECT_EQ(PermissionsData::PageAccess::kWithheld,
get_access(example_com, example_com_origin,
content::RESOURCE_TYPE_MAIN_FRAME));
// However, if a sub-resource request is made to example.com from an initiator
// that the extension has access to, access is allowed. This is functionally
// necessary for any extension with webRequest to work with the runtime host
// permissions feature. See https://crbug.com/851722.
EXPECT_EQ(PermissionsData::PageAccess::kAllowed,
get_access(example_com, chromium_org_origin,
content::RESOURCE_TYPE_SUB_RESOURCE));
EXPECT_EQ(PermissionsData::PageAccess::kAllowed,
get_access(example_com, chromium_org_origin, base::nullopt));
EXPECT_EQ(PermissionsData::PageAccess::kWithheld,
get_access(example_com, chromium_org_origin,
content::RESOURCE_TYPE_SUB_FRAME));
EXPECT_EQ(PermissionsData::PageAccess::kWithheld,
get_access(example_com, chromium_org_origin,
content::RESOURCE_TYPE_MAIN_FRAME));
// With access to the requested origin, access is always allowed for
// REQUIRE_HOST_PERMISSION_FOR_URL, independent of initiator.
for (const auto& initiator : initiators) {
for (const auto& resource_type : resource_types) {
EXPECT_EQ(PermissionsData::PageAccess::kAllowed,
get_access(chromium_org, initiator, resource_type));
}
}
}
TEST(ExtensionWebRequestPermissions,
RequireAccessToURLAndInitiatorWithWithheldPermissions) {
// The InfoMap requires methods to be called on the IO thread. Fake it.
content::TestBrowserThreadBundle thread_bundle(
content::TestBrowserThreadBundle::IO_MAINLOOP);
const char* kGoogleCom = "https://google.com/";
const char* kExampleCom = "https://example.com/";
const char* kYahooCom = "https://yahoo.com";
// Set up the extension to have access to kGoogleCom and withheld access to
// kExampleCom.
scoped_refptr<const Extension> extension =
ExtensionBuilder("ext").AddPermissions({kGoogleCom, kExampleCom}).Build();
URLPatternSet kActivePatternSet(
{URLPattern(Extension::kValidHostPermissionSchemes, kGoogleCom)});
URLPatternSet kWithheldPatternSet(
{URLPattern(Extension::kValidHostPermissionSchemes, kExampleCom)});
extension->permissions_data()->SetPermissions(
std::make_unique<PermissionSet>(
APIPermissionSet(), ManifestPermissionSet(), kActivePatternSet,
kActivePatternSet), // active permissions.
std::make_unique<PermissionSet>(
APIPermissionSet(), ManifestPermissionSet(), kWithheldPatternSet,
kWithheldPatternSet) /* withheld permissions */);
scoped_refptr<InfoMap> info_map = base::MakeRefCounted<InfoMap>();
info_map->AddExtension(extension.get(), base::Time(), false, false);
auto get_access = [extension, info_map](
const GURL& url,
const base::Optional<url::Origin>& initiator,
const base::Optional<content::ResourceType>&
resource_type) {
constexpr int kTabId = 42;
constexpr WebRequestPermissions::HostPermissionsCheck kPermissionsCheck =
WebRequestPermissions::REQUIRE_HOST_PERMISSION_FOR_URL_AND_INITIATOR;
return WebRequestPermissions::CanExtensionAccessURL(
info_map.get(), extension->id(), url, kTabId,
false /* crosses incognito */, kPermissionsCheck, initiator,
resource_type);
};
using PageAccess = PermissionsData::PageAccess;
const GURL kAllowedUrl(kGoogleCom);
const GURL kWithheldUrl(kExampleCom);
const GURL kDeniedUrl(kYahooCom);
const url::Origin kAllowedOrigin(url::Origin::Create(kAllowedUrl));
const url::Origin kWithheldOrigin(url::Origin::Create(kWithheldUrl));
const url::Origin kDeniedOrigin(url::Origin::Create(kDeniedUrl));
const url::Origin kOpaqueOrigin;
struct {
base::Optional<url::Origin> initiator;
GURL url;
PermissionsData::PageAccess expected_access_subresource;
PermissionsData::PageAccess expected_access_navigation;
} cases[] = {
{base::nullopt, kAllowedUrl, PageAccess::kAllowed, PageAccess::kAllowed},
{base::nullopt, kWithheldUrl, PageAccess::kWithheld,
PageAccess::kWithheld},
{base::nullopt, kDeniedUrl, PageAccess::kDenied, PageAccess::kDenied},
{kOpaqueOrigin, kAllowedUrl, PageAccess::kAllowed, PageAccess::kAllowed},
{kOpaqueOrigin, kWithheldUrl, PageAccess::kWithheld,
PageAccess::kWithheld},
{kOpaqueOrigin, kDeniedUrl, PageAccess::kDenied, PageAccess::kDenied},
{kDeniedOrigin, kAllowedUrl, PageAccess::kDenied, PageAccess::kAllowed},
{kDeniedOrigin, kWithheldUrl, PageAccess::kDenied, PageAccess::kWithheld},
{kDeniedOrigin, kDeniedUrl, PageAccess::kDenied, PageAccess::kDenied},
{kAllowedOrigin, kDeniedUrl, PageAccess::kDenied, PageAccess::kDenied},
{kWithheldOrigin, kDeniedUrl, PageAccess::kDenied, PageAccess::kDenied},
{kWithheldOrigin, kWithheldUrl, PageAccess::kWithheld,
PageAccess::kWithheld},
{kWithheldOrigin, kAllowedUrl, PageAccess::kWithheld,
PageAccess::kAllowed},
{kAllowedOrigin, kWithheldUrl, PageAccess::kAllowed,
PageAccess::kWithheld},
{kAllowedOrigin, kAllowedUrl, PageAccess::kAllowed, PageAccess::kAllowed},
};
for (const auto& test_case : cases) {
SCOPED_TRACE(base::StringPrintf(
"url-%s initiator-%s", test_case.url.spec().c_str(),
test_case.initiator ? test_case.initiator->Serialize().c_str()
: "empty"));
EXPECT_EQ(get_access(test_case.url, test_case.initiator,
content::RESOURCE_TYPE_SUB_RESOURCE),
test_case.expected_access_subresource);
EXPECT_EQ(get_access(test_case.url, test_case.initiator, base::nullopt),
test_case.expected_access_subresource);
EXPECT_EQ(get_access(test_case.url, test_case.initiator,
content::RESOURCE_TYPE_SUB_FRAME),
test_case.expected_access_navigation);
EXPECT_EQ(get_access(test_case.url, test_case.initiator,
content::RESOURCE_TYPE_MAIN_FRAME),
test_case.expected_access_navigation);
}
}
} // namespace extensions