blob: 51fb2b2a7df262c3ccb43e91c53eada519801020 [file] [log] [blame]
// Copyright 2017 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/declarative_net_request/ruleset_manager.h"
#include <algorithm>
#include <tuple>
#include <utility>
#include "base/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/optional.h"
#include "base/stl_util.h"
#include "components/web_cache/browser/web_cache_manager.h"
#include "content/public/browser/resource_request_info.h"
#include "extensions/browser/api/declarative_net_request/composite_matcher.h"
#include "extensions/browser/api/declarative_net_request/constants.h"
#include "extensions/browser/api/declarative_net_request/utils.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/api/web_request/web_request_info.h"
#include "extensions/browser/api/web_request/web_request_permissions.h"
#include "extensions/browser/info_map.h"
#include "extensions/common/api/declarative_net_request.h"
#include "extensions/common/api/declarative_net_request/utils.h"
#include "extensions/common/constants.h"
#include "net/http/http_request_headers.h"
#include "url/origin.h"
namespace extensions {
namespace declarative_net_request {
namespace {
namespace flat_rule = url_pattern_index::flat;
namespace dnr_api = api::declarative_net_request;
using PageAccess = PermissionsData::PageAccess;
// Describes the different cases pertaining to initiator checks to find the main
// frame url for a main frame subresource.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class PageAllowingInitiatorCheck {
kInitiatorAbsent = 0,
kNeitherCandidateMatchesInitiator = 1,
kCommittedCandidateMatchesInitiator = 2,
kPendingCandidateMatchesInitiator = 3,
kBothCandidatesMatchInitiator = 4,
kMaxValue = kBothCandidatesMatchInitiator,
};
constexpr const char kSetCookieResponseHeader[] = "set-cookie";
// Returns true if |request| came from a page from the set of
// |allowed_pages|. This necessitates finding the main frame url
// corresponding to |request|. The logic behind how this is done is subtle and
// as follows:
// - Requests made by the browser (not including navigation/frame requests) or
// service worker: These requests don't correspond to a render frame and
// hence they are not considered for allowing using the page
// allowing API.
// - Requests that correspond to a page: These include:
// - Main frame request: To check if it is allowed, check the request
// url against the set of allowed pages.
// - Main frame subresource request: We might not be able to
// deterministically map a main frame subresource to the main frame url.
// This is because when a main frame subresource request reaches the
// browser, the main frame navigation would have been committed in the
// renderer, but the browser may not have been notified of the commit.
// Hence the FrameData for the request may not have the correct value for
// the |last_committed_main_frame_url|. To get around this we use
// FrameData's |pending_main_frame_url| which is populated in
// WebContentsObserver::ReadyToCommitNavigation. This happens before the
// renderer is asked to commit the navigation.
// - Subframe subresources: When a subframe subresource request reaches the
// browser, it is assured that the browser knows about its parent frame
// commit. For these requests, use the |last_committed_main_frame_url| and
// match it against the set of allowed pages.
bool IsRequestPageAllowed(const WebRequestInfo& request,
const URLPatternSet& allowed_pages) {
if (allowed_pages.is_empty())
return false;
// If this is a main frame request, |request.url| will be the main frame url.
if (request.type == content::ResourceType::kMainFrame)
return allowed_pages.MatchesURL(request.url);
// This should happen for:
// - Requests not corresponding to a render frame e.g. non-navigation
// browser requests or service worker requests.
// - Requests made by a render frame but when we don't have cached FrameData
// for the request. This should occur rarely and is tracked by the
// "Extensions.ExtensionFrameMapCacheHit" histogram
if (!request.frame_data)
return false;
const bool evaluate_pending_main_frame_url =
request.frame_data->pending_main_frame_url &&
*request.frame_data->pending_main_frame_url !=
request.frame_data->last_committed_main_frame_url;
if (!evaluate_pending_main_frame_url) {
return allowed_pages.MatchesURL(
request.frame_data->last_committed_main_frame_url);
}
// |pending_main_frame_url| should only be set for main-frame subresource
// loads.
DCHECK_EQ(ExtensionApiFrameIdMap::kTopFrameId, request.frame_data->frame_id);
auto log_uma = [](PageAllowingInitiatorCheck value) {
UMA_HISTOGRAM_ENUMERATION(
"Extensions.DeclarativeNetRequest.PageWhitelistingInitiatorCheck",
value);
};
// At this point, we are evaluating a main-frame subresource. There are two
// candidate main frame urls - |pending_main_frame_url| and
// |last_committed_main_frame_url|. To predict the correct main frame url,
// compare the request initiator (origin of the requesting frame i.e. origin
// of the main frame in this case) with the candidate urls' origins. If only
// one of the candidate url's origin matches the request initiator, we can be
// reasonably sure that it is the correct main frame url.
if (!request.initiator) {
log_uma(PageAllowingInitiatorCheck::kInitiatorAbsent);
} else {
const bool initiator_matches_pending_url =
url::Origin::Create(*request.frame_data->pending_main_frame_url) ==
*request.initiator;
const bool initiator_matches_committed_url =
url::Origin::Create(
request.frame_data->last_committed_main_frame_url) ==
*request.initiator;
if (initiator_matches_pending_url && !initiator_matches_committed_url) {
// We predict that |pending_main_frame_url| is the actual main frame url.
log_uma(PageAllowingInitiatorCheck::kPendingCandidateMatchesInitiator);
return allowed_pages.MatchesURL(
*request.frame_data->pending_main_frame_url);
}
if (initiator_matches_committed_url && !initiator_matches_pending_url) {
// We predict that |last_committed_main_frame_url| is the actual main
// frame url.
log_uma(PageAllowingInitiatorCheck::kCommittedCandidateMatchesInitiator);
return allowed_pages.MatchesURL(
request.frame_data->last_committed_main_frame_url);
}
if (initiator_matches_pending_url && initiator_matches_committed_url) {
log_uma(PageAllowingInitiatorCheck::kBothCandidatesMatchInitiator);
} else {
DCHECK(!initiator_matches_pending_url);
DCHECK(!initiator_matches_committed_url);
log_uma(PageAllowingInitiatorCheck::kNeitherCandidateMatchesInitiator);
}
}
// If we are not able to correctly predict the main frame url, simply test
// against both the possible URLs. This means a small proportion of main frame
// subresource requests might be incorrectly allowed by the page
// allowing API.
return allowed_pages.MatchesURL(
request.frame_data->last_committed_main_frame_url) ||
allowed_pages.MatchesURL(*request.frame_data->pending_main_frame_url);
}
bool ShouldCollapseResourceType(flat_rule::ElementType type) {
// TODO(crbug.com/848842): Add support for other element types like
// OBJECT.
return type == flat_rule::ElementType_IMAGE ||
type == flat_rule::ElementType_SUBDOCUMENT;
}
void NotifyRequestWithheld(const ExtensionId& extension_id,
const WebRequestInfo& request) {
DCHECK(ExtensionsAPIClient::Get());
ExtensionsAPIClient::Get()->NotifyWebRequestWithheld(
request.render_process_id, request.frame_id, extension_id);
}
// Populates the list of headers corresponding to |mask|.
void PopulateHeadersFromMask(uint8_t mask,
std::vector<const char*>* request_headers,
std::vector<const char*>* response_headers) {
DCHECK(request_headers);
DCHECK(response_headers);
uint8_t bit = 0;
// Iterate over each RemoveHeaderType value.
for (int i = 0; mask && i <= dnr_api::REMOVE_HEADER_TYPE_LAST; ++i) {
switch (i) {
case dnr_api::REMOVE_HEADER_TYPE_NONE:
break;
case dnr_api::REMOVE_HEADER_TYPE_COOKIE:
bit = kRemoveHeadersMask_Cookie;
if (mask & bit) {
mask &= ~bit;
request_headers->push_back(net::HttpRequestHeaders::kCookie);
}
break;
case dnr_api::REMOVE_HEADER_TYPE_REFERER:
bit = kRemoveHeadersMask_Referer;
if (mask & bit) {
mask &= ~bit;
request_headers->push_back(net::HttpRequestHeaders::kReferer);
}
break;
case dnr_api::REMOVE_HEADER_TYPE_SETCOOKIE:
bit = kRemoveHeadersMask_SetCookie;
if (mask & bit) {
mask &= ~bit;
response_headers->push_back(kSetCookieResponseHeader);
}
break;
}
}
}
} // namespace
RulesetManager::Action::Action(Action::Type type) : type(type) {}
RulesetManager::Action::~Action() = default;
RulesetManager::Action::Action(Action&&) = default;
RulesetManager::Action& RulesetManager::Action::operator=(Action&&) = default;
RulesetManager::RulesetManager(const InfoMap* info_map) : info_map_(info_map) {
DCHECK(info_map_);
// RulesetManager can be created on any sequence.
DETACH_FROM_SEQUENCE(sequence_checker_);
}
RulesetManager::~RulesetManager() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void RulesetManager::AddRuleset(const ExtensionId& extension_id,
std::unique_ptr<CompositeMatcher> matcher,
URLPatternSet allowed_pages) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(IsAPIAvailable());
bool inserted;
std::tie(std::ignore, inserted) =
rulesets_.emplace(extension_id, info_map_->GetInstallTime(extension_id),
std::move(matcher), std::move(allowed_pages));
DCHECK(inserted) << "AddRuleset called twice in succession for "
<< extension_id;
if (test_observer_)
test_observer_->OnRulesetCountChanged(rulesets_.size());
// Clear the renderers' cache so that they take the new rules into account.
ClearRendererCacheOnNavigation();
}
void RulesetManager::RemoveRuleset(const ExtensionId& extension_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(IsAPIAvailable());
auto compare_by_id =
[&extension_id](const ExtensionRulesetData& ruleset_data) {
return ruleset_data.extension_id == extension_id;
};
DCHECK(std::find_if(rulesets_.begin(), rulesets_.end(), compare_by_id) !=
rulesets_.end())
<< "RemoveRuleset called without a corresponding AddRuleset for "
<< extension_id;
base::EraseIf(rulesets_, compare_by_id);
if (test_observer_)
test_observer_->OnRulesetCountChanged(rulesets_.size());
// Clear the renderers' cache so that they take the removed rules into
// account.
ClearRendererCacheOnNavigation();
}
CompositeMatcher* RulesetManager::GetMatcherForExtension(
const ExtensionId& extension_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(IsAPIAvailable());
// This is O(n) but it's ok since the number of extensions will be small and
// we have to maintain the rulesets sorted in decreasing order of installation
// time.
auto iter =
std::find_if(rulesets_.begin(), rulesets_.end(),
[&extension_id](const ExtensionRulesetData& ruleset) {
return ruleset.extension_id == extension_id;
});
// There must be ExtensionRulesetData corresponding to this |extension_id|.
if (iter == rulesets_.end())
return nullptr;
DCHECK(iter->matcher);
return iter->matcher.get();
}
void RulesetManager::UpdateAllowedPages(const ExtensionId& extension_id,
URLPatternSet allowed_pages) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(IsAPIAvailable());
// This is O(n) but it's ok since the number of extensions will be small and
// we have to maintain the rulesets sorted in decreasing order of installation
// time.
auto iter =
std::find_if(rulesets_.begin(), rulesets_.end(),
[&extension_id](const ExtensionRulesetData& ruleset) {
return ruleset.extension_id == extension_id;
});
// There must be ExtensionRulesetData corresponding to this |extension_id|.
DCHECK(iter != rulesets_.end());
iter->allowed_pages = std::move(allowed_pages);
// Clear the renderers' cache so that they take the updated allowed pages
// into account.
ClearRendererCacheOnNavigation();
}
const RulesetManager::Action& RulesetManager::EvaluateRequest(
const WebRequestInfo& request,
bool is_incognito_context) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Note: it's safe to cache the action on WebRequestInfo without worrying
// about |is_incognito_context| since a WebRequestInfo object will not be
// shared between different contexts. Hence the value of
// |is_incognito_context| will stay the same for a given |request|. This also
// assumes that the core state of the WebRequestInfo isn't changed between the
// different EvaluateRequest invocations.
if (!request.dnr_action)
request.dnr_action = EvaluateRequestInternal(request, is_incognito_context);
return *request.dnr_action;
}
bool RulesetManager::HasAnyExtraHeadersMatcher() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (const auto& ruleset : rulesets_) {
if (ruleset.matcher->HasAnyExtraHeadersMatcher())
return true;
}
return false;
}
bool RulesetManager::HasExtraHeadersMatcherForRequest(
const WebRequestInfo& request,
bool is_incognito_context) const {
const Action& action = EvaluateRequest(request, is_incognito_context);
// We only support removing a subset of extra headers currently. If that
// changes, the implementation here should change as well.
static_assert(flat::ActionIndex_count == 6,
"Modify this method to ensure HasExtraHeadersMatcherForRequest "
"is updated as new actions are added.");
return action.type == Action::Type::REMOVE_HEADERS;
}
void RulesetManager::SetObserverForTest(TestObserver* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
test_observer_ = observer;
}
RulesetManager::ExtensionRulesetData::ExtensionRulesetData(
const ExtensionId& extension_id,
const base::Time& extension_install_time,
std::unique_ptr<CompositeMatcher> matcher,
URLPatternSet allowed_pages)
: extension_id(extension_id),
extension_install_time(extension_install_time),
matcher(std::move(matcher)),
allowed_pages(std::move(allowed_pages)) {}
RulesetManager::ExtensionRulesetData::~ExtensionRulesetData() = default;
RulesetManager::ExtensionRulesetData::ExtensionRulesetData(
ExtensionRulesetData&& other) = default;
RulesetManager::ExtensionRulesetData& RulesetManager::ExtensionRulesetData::
operator=(ExtensionRulesetData&& other) = default;
bool RulesetManager::ExtensionRulesetData::operator<(
const ExtensionRulesetData& other) const {
// Sort based on *descending* installation time, using extension id to break
// ties.
return std::tie(extension_install_time, extension_id) >
std::tie(other.extension_install_time, other.extension_id);
}
base::Optional<RulesetManager::Action> RulesetManager::GetBlockOrCollapseAction(
const std::vector<const ExtensionRulesetData*>& rulesets,
const RequestParams& params) const {
for (const ExtensionRulesetData* ruleset : rulesets) {
if (ruleset->matcher->ShouldBlockRequest(params)) {
return ShouldCollapseResourceType(params.element_type)
? Action(Action::Type::COLLAPSE)
: Action(Action::Type::BLOCK);
}
}
return base::nullopt;
}
base::Optional<RulesetManager::Action> RulesetManager::GetRedirectAction(
const std::vector<const ExtensionRulesetData*>& rulesets,
const RequestParams& params) const {
DCHECK(std::is_sorted(rulesets.begin(), rulesets.end(),
[](const ExtensionRulesetData* a,
const ExtensionRulesetData* b) { return *a < *b; }));
// Redirecting WebSocket handshake request is prohibited.
if (params.element_type == flat_rule::ElementType_WEBSOCKET)
return base::nullopt;
// This iterates in decreasing order of extension installation time. Hence
// more recently installed extensions get higher priority in choosing the
// redirect url.
for (const ExtensionRulesetData* ruleset : rulesets) {
GURL redirect_url;
if (ruleset->matcher->ShouldRedirectRequest(params, &redirect_url)) {
Action action(Action::Type::REDIRECT);
action.redirect_url = std::move(redirect_url);
return action;
}
}
return base::nullopt;
}
base::Optional<RulesetManager::Action> RulesetManager::GetRemoveHeadersAction(
const std::vector<const ExtensionRulesetData*>& rulesets,
const RequestParams& params) const {
uint8_t mask = 0;
for (const ExtensionRulesetData* ruleset : rulesets)
mask |= ruleset->matcher->GetRemoveHeadersMask(params, mask);
if (!mask)
return base::nullopt;
Action action(Action::Type::REMOVE_HEADERS);
PopulateHeadersFromMask(mask, &action.request_headers_to_remove,
&action.response_headers_to_remove);
return action;
}
RulesetManager::Action RulesetManager::EvaluateRequestInternal(
const WebRequestInfo& request,
bool is_incognito_context) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!request.dnr_action);
if (!ShouldEvaluateRequest(request))
return Action(Action::Type::NONE);
if (test_observer_)
test_observer_->OnEvaluateRequest(request, is_incognito_context);
if (rulesets_.empty())
return Action(Action::Type::NONE);
SCOPED_UMA_HISTOGRAM_TIMER(
"Extensions.DeclarativeNetRequest.EvaluateRequestTime.AllExtensions2");
const RequestParams params(request);
const int tab_id = request.frame_data ? request.frame_data->tab_id
: extension_misc::kUnknownTabId;
// |crosses_incognito| is used to ensure that a split mode extension process
// can't intercept requests from a cross browser context. Since declarative
// net request API doesn't use event listeners in a background process, it is
// irrelevant here.
const bool crosses_incognito = false;
// Filter the rulesets to evaluate.
std::vector<const ExtensionRulesetData*> rulesets_to_evaluate;
for (const ExtensionRulesetData& ruleset : rulesets_) {
if (!ShouldEvaluateRulesetForRequest(ruleset, request,
is_incognito_context)) {
continue;
}
// If the extension doesn't have permission to the request, then skip this
// ruleset. Note: we are not checking for host permissions here.
// DO_NOT_CHECK_HOST is strictly less restrictive than
// REQUIRE_HOST_PERMISSION_FOR_URL_AND_INITIATOR.
PageAccess page_access = WebRequestPermissions::CanExtensionAccessURL(
info_map_, ruleset.extension_id, request.url, tab_id, crosses_incognito,
WebRequestPermissions::DO_NOT_CHECK_HOST, request.initiator,
request.type);
DCHECK_NE(PageAccess::kWithheld, page_access);
if (page_access != PageAccess::kAllowed)
continue;
rulesets_to_evaluate.push_back(&ruleset);
}
// If the request is blocked, no further modifications can happen.
base::Optional<Action> action =
GetBlockOrCollapseAction(rulesets_to_evaluate, params);
if (action)
return std::move(*action);
// Redirecting a request requires host permissions to the request url and
// its initiator.
std::vector<const ExtensionRulesetData*>
rulesets_to_evaluate_with_host_permissions;
for (const ExtensionRulesetData* ruleset : rulesets_to_evaluate) {
PageAccess page_access = WebRequestPermissions::CanExtensionAccessURL(
info_map_, ruleset->extension_id, request.url, tab_id,
crosses_incognito,
WebRequestPermissions::REQUIRE_HOST_PERMISSION_FOR_URL_AND_INITIATOR,
request.initiator, request.type);
if (page_access != PageAccess::kAllowed) {
// Notify the web request was withheld if the extension would have
// redirected the request.
// Note: The following check should be modified if more actions needing
// host permissions are added.
GURL ignore;
if (page_access == PageAccess::kWithheld &&
ruleset->matcher->ShouldRedirectRequest(params, &ignore)) {
NotifyRequestWithheld(ruleset->extension_id, request);
}
continue;
}
rulesets_to_evaluate_with_host_permissions.push_back(ruleset);
}
// If the request is redirected, no further modifications can happen. A new
// request will be created and subsequently evaluated.
action =
GetRedirectAction(rulesets_to_evaluate_with_host_permissions, params);
if (action)
return std::move(*action);
// Removing headers doesn't require host permissions.
// Note: If we add other "non-destructive" actions (i.e., actions that don't
// end the request), we should combine them with the remove-headers action.
action = GetRemoveHeadersAction(rulesets_to_evaluate, params);
if (action)
return std::move(*action);
return Action(Action::Type::NONE);
}
bool RulesetManager::ShouldEvaluateRequest(
const WebRequestInfo& request) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Ensure clients filter out sensitive requests.
DCHECK(!WebRequestPermissions::HideRequest(info_map_, request));
if (!IsAPIAvailable()) {
DCHECK(rulesets_.empty());
return false;
}
// Prevent extensions from modifying any resources on the chrome-extension
// scheme. Practically, this has the effect of not allowing an extension to
// modify its own resources (The extension wouldn't have the permission to
// other extension origins anyway).
if (request.url.SchemeIs(kExtensionScheme))
return false;
return true;
}
bool RulesetManager::ShouldEvaluateRulesetForRequest(
const ExtensionRulesetData& ruleset,
const WebRequestInfo& request,
bool is_incognito_context) const {
// Only extensions enabled in incognito should have access to requests in an
// incognito context.
if (is_incognito_context &&
!info_map_->IsIncognitoEnabled(ruleset.extension_id)) {
return false;
}
if (IsRequestPageAllowed(request, ruleset.allowed_pages))
return false;
return true;
}
} // namespace declarative_net_request
} // namespace extensions