blob: 54834cd004af83bd8811c7cb70d5882043a3c6a3 [file]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/subresource_filter/core/common/indexed_ruleset.h"
#include <memory>
#include <string_view>
#include "base/check.h"
#include "base/strings/utf_string_conversions.h"
#include "components/subresource_filter/core/common/first_party_origin.h"
#include "components/subresource_filter/core/common/load_policy.h"
#include "components/subresource_filter/core/common/test_ruleset_utils.h"
#include "components/url_pattern_index/proto/rules.pb.h"
#include "components/url_pattern_index/url_pattern.h"
#include "components/url_pattern_index/url_rule_test_support.h"
#include "components/url_pattern_index/url_rule_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace subresource_filter {
using ::subresource_filter::testing::StyleRuleParams;
namespace proto = url_pattern_index::proto;
using url_pattern_index::UrlPattern;
using url_pattern_index::testing::MakeUrlRule;
class SubresourceFilterIndexedRulesetTest : public ::testing::Test {
public:
SubresourceFilterIndexedRulesetTest() { Reset(); }
SubresourceFilterIndexedRulesetTest(
const SubresourceFilterIndexedRulesetTest&) = delete;
SubresourceFilterIndexedRulesetTest& operator=(
const SubresourceFilterIndexedRulesetTest&) = delete;
protected:
LoadPolicy GetLoadPolicy(
std::string_view url,
std::string_view document_origin = "",
proto::ElementType element_type = url_pattern_index::testing::kOther,
bool disable_generic_rules = false,
const url_pattern_index::flat::UrlRule** out_rule = nullptr) const {
CHECK(matcher_);
return matcher_->GetLoadPolicyForResourceLoad(
GURL(url),
FirstPartyOrigin(
url_pattern_index::testing::GetOrigin(document_origin)),
element_type, disable_generic_rules, out_rule);
}
bool MatchingRule(
std::string_view url,
std::string_view document_origin = "",
proto::ElementType element_type = url_pattern_index::testing::kOther,
bool disable_generic_rules = false) const {
CHECK(matcher_);
return matcher_->MatchedUrlRule(
GURL(url),
FirstPartyOrigin(
url_pattern_index::testing::GetOrigin(document_origin)),
element_type, disable_generic_rules) != nullptr;
}
bool ShouldDeactivate(std::string_view document_url,
std::string_view parent_document_origin = "",
proto::ActivationType activation_type =
url_pattern_index::testing::kNoActivation) const {
CHECK(matcher_);
return matcher_->ShouldDisableFilteringForDocument(
GURL(document_url),
url_pattern_index::testing::GetOrigin(parent_document_origin),
activation_type);
}
bool AddUrlRule(const proto::UrlRule& rule) {
return indexer_->AddUrlRule(rule);
}
bool AddSimpleRule(std::string_view url_pattern) {
return AddUrlRule(MakeUrlRule(
UrlPattern(url_pattern, url_pattern_index::testing::kSubstring)));
}
bool AddSimpleAllowlistRule(std::string_view url_pattern) {
auto rule = MakeUrlRule(
UrlPattern(url_pattern, url_pattern_index::testing::kSubstring));
rule.set_semantics(proto::RULE_SEMANTICS_ALLOWLIST);
return AddUrlRule(rule);
}
bool AddSimpleAllowlistRule(std::string_view url_pattern,
int32_t activation_types) {
auto rule = MakeUrlRule(
UrlPattern(url_pattern, url_pattern_index::testing::kSubstring));
rule.set_semantics(proto::RULE_SEMANTICS_ALLOWLIST);
rule.clear_element_types();
rule.set_activation_types(activation_types);
return AddUrlRule(rule);
}
bool AddStyleRule(const StyleRuleParams& params) {
return indexer_->AddStyleRuleFromProto(testing::CreateStyleRule(params));
}
void Finish() {
indexer_->Finish();
matcher_ = std::make_unique<IndexedRulesetMatcher>(indexer_->data());
}
void Reset() {
matcher_.reset(nullptr);
indexer_ = std::make_unique<RulesetIndexer>(0);
}
std::unique_ptr<RulesetIndexer> indexer_;
std::unique_ptr<IndexedRulesetMatcher> matcher_;
};
TEST_F(SubresourceFilterIndexedRulesetTest, EmptyRuleset) {
Finish();
EXPECT_EQ(LoadPolicy::ALLOW, GetLoadPolicy(""));
EXPECT_EQ(LoadPolicy::ALLOW, GetLoadPolicy("http://example.com"));
EXPECT_EQ(LoadPolicy::ALLOW,
GetLoadPolicy("http://another.example.com?param=val"));
}
TEST_F(SubresourceFilterIndexedRulesetTest, NoRuleApplies) {
ASSERT_TRUE(AddSimpleRule("?filter_out="));
ASSERT_TRUE(AddSimpleRule("&filter_out="));
Finish();
EXPECT_EQ(LoadPolicy::ALLOW, GetLoadPolicy("http://example.com"));
EXPECT_EQ(LoadPolicy::ALLOW, GetLoadPolicy("http://example.com?filter_not"));
}
TEST_F(SubresourceFilterIndexedRulesetTest, NoRuleApplies_OutRuleParameter) {
ASSERT_TRUE(AddSimpleRule("?filter_out="));
Finish();
const url_pattern_index::flat::UrlRule* rule = nullptr;
EXPECT_EQ(LoadPolicy::ALLOW,
GetLoadPolicy("http://example.com", /*document_origin=*/"",
/*=element_type=*/url_pattern_index::testing::kOther,
/*disable_generic_rules=*/false, /*out_rule=*/&rule));
EXPECT_FALSE(rule);
}
TEST_F(SubresourceFilterIndexedRulesetTest, SimpleBlocklist) {
ASSERT_TRUE(AddSimpleRule("?param="));
Finish();
EXPECT_EQ(LoadPolicy::ALLOW, GetLoadPolicy("https://example.com"));
EXPECT_EQ(LoadPolicy::DISALLOW,
GetLoadPolicy("http://example.org?param=image1"));
}
TEST_F(SubresourceFilterIndexedRulesetTest, SimpleBlocklist_OutRuleParameter) {
ASSERT_TRUE(AddSimpleRule("?param="));
Finish();
const url_pattern_index::flat::UrlRule* rule = nullptr;
EXPECT_EQ(
LoadPolicy::DISALLOW,
GetLoadPolicy("http://example.com?param=image1", /*document_origin=*/"",
/*=element_type=*/url_pattern_index::testing::kOther,
/*disable_generic_rules=*/false, /*out_rule=*/&rule));
EXPECT_TRUE(rule);
EXPECT_EQ(url_pattern_index::FlatUrlRuleToFilterlistString(rule), "?param=");
}
TEST_F(SubresourceFilterIndexedRulesetTest, SimpleBlocklistSubdocument) {
ASSERT_TRUE(AddSimpleRule("?param="));
Finish();
EXPECT_EQ(LoadPolicy::ALLOW, GetLoadPolicy("https://example.com"));
EXPECT_EQ(LoadPolicy::DISALLOW,
GetLoadPolicy("http://example.org?param=image1",
/*document_origin=*/"",
url_pattern_index::testing::kSubdocument));
}
TEST_F(SubresourceFilterIndexedRulesetTest, SimpleAllowlist) {
ASSERT_TRUE(AddSimpleAllowlistRule("example.com/?filter_out="));
Finish();
// This should not return EXPLICITLY_ALLOW because there is no corresponding
// blocklist rule for the allowlist rule. To optimize speed, allowlist rules
// are only checked if a rule was matched with a blocklist rule unless it
// is a subdocument resource.
EXPECT_EQ(LoadPolicy::ALLOW,
GetLoadPolicy("https://example.com?filter_out=true"));
}
TEST_F(SubresourceFilterIndexedRulesetTest, SimpleAllowlist_OutRuleParameter) {
ASSERT_TRUE(AddSimpleAllowlistRule("example.com/?filter_out="));
Finish();
const url_pattern_index::flat::UrlRule* rule = nullptr;
EXPECT_EQ(LoadPolicy::ALLOW,
GetLoadPolicy("https://example.com?filter_out=true",
/*document_origin=*/"",
/*=element_type=*/url_pattern_index::testing::kOther,
/*disable_generic_rules=*/false, /*out_rule=*/&rule));
EXPECT_FALSE(rule);
}
TEST_F(SubresourceFilterIndexedRulesetTest, SimpleAllowlistSubdocument) {
ASSERT_TRUE(AddSimpleAllowlistRule("example.com/?filter_out="));
Finish();
// Verify allowlist rules are always checked for subdocument element types.
EXPECT_EQ(LoadPolicy::EXPLICITLY_ALLOW,
GetLoadPolicy("https://example.com?filter_out=true",
/*document_origin=*/"",
url_pattern_index::testing::kSubdocument));
}
TEST_F(SubresourceFilterIndexedRulesetTest,
SimpleAllowlistWithMatchingBlocklist) {
ASSERT_TRUE(AddSimpleRule("example.com/?filter_out="));
ASSERT_TRUE(AddSimpleAllowlistRule("example.com/?filter_out="));
Finish();
EXPECT_EQ(LoadPolicy::EXPLICITLY_ALLOW,
GetLoadPolicy("https://example.com?filter_out=true"));
}
TEST_F(SubresourceFilterIndexedRulesetTest,
SimpleAllowlistWithMatchingBlocklist_OutRuleParameter) {
ASSERT_TRUE(AddSimpleRule("example.com/?filter_out="));
ASSERT_TRUE(AddSimpleAllowlistRule("example.com/?filter_out="));
Finish();
const url_pattern_index::flat::UrlRule* rule = nullptr;
EXPECT_EQ(LoadPolicy::EXPLICITLY_ALLOW,
GetLoadPolicy("https://example.com?filter_out=true",
/*document_origin=*/"",
/*=element_type=*/url_pattern_index::testing::kOther,
/*disable_generic_rules=*/false, /*out_rule=*/&rule));
EXPECT_FALSE(rule);
}
TEST_F(SubresourceFilterIndexedRulesetTest,
SimpleAllowlistWithMatchingBlocklistSubdocument) {
ASSERT_TRUE(AddSimpleRule("example.com/?filter_out="));
ASSERT_TRUE(AddSimpleAllowlistRule("example.com/?filter_out="));
Finish();
EXPECT_EQ(LoadPolicy::EXPLICITLY_ALLOW,
GetLoadPolicy("https://example.com?filter_out=true",
/*document_origin=*/"",
url_pattern_index::testing::kSubdocument));
}
// Ensure patterns containing non-ascii characters are disallowed.
TEST_F(SubresourceFilterIndexedRulesetTest, NonAsciiPatterns) {
// non-ascii character é.
std::string non_ascii = base::WideToUTF8(L"\u00E9");
ASSERT_FALSE(AddSimpleRule(non_ascii));
Finish();
EXPECT_EQ(LoadPolicy::ALLOW,
GetLoadPolicy("https://example.com/q=" + non_ascii));
}
// Ensure that specifying non-ascii characters in percent encoded form in
// patterns works.
TEST_F(SubresourceFilterIndexedRulesetTest, PercentEncodedPatterns) {
// Percent encoded form of é.
ASSERT_TRUE(AddSimpleRule("%C3%A9"));
Finish();
EXPECT_EQ(LoadPolicy::DISALLOW, GetLoadPolicy("https://example.com/q=" +
base::WideToUTF8(L"\u00E9")));
}
// Ensures that specifying patterns in punycode works for matching IDN domains.
TEST_F(SubresourceFilterIndexedRulesetTest, IDNHosts) {
// ҏӊԟҭв.com
const std::string punycode = "xn--b1a9p8c1e8r.com";
ASSERT_TRUE(AddSimpleRule(punycode));
Finish();
EXPECT_EQ(LoadPolicy::DISALLOW, GetLoadPolicy("https://" + punycode));
EXPECT_EQ(LoadPolicy::DISALLOW,
GetLoadPolicy(base::WideToUTF8(
L"https://\x048f\x04ca\x051f\x04ad\x0432.com")));
}
// Ensure patterns containing non-ascii domains are disallowed.
TEST_F(SubresourceFilterIndexedRulesetTest, NonAsciiDomain) {
const char* kUrl = "http://example.com";
// ґғ.com
std::string non_ascii_domain = base::WideToUTF8(L"\x0491\x0493.com");
auto rule =
MakeUrlRule(UrlPattern(kUrl, url_pattern_index::testing::kSubstring));
url_pattern_index::testing::AddInitiatorDomains({non_ascii_domain}, &rule);
ASSERT_FALSE(AddUrlRule(rule));
rule = MakeUrlRule(UrlPattern(kUrl, url_pattern_index::testing::kSubstring));
std::string non_ascii_excluded_domain = "~" + non_ascii_domain;
url_pattern_index::testing::AddInitiatorDomains({non_ascii_excluded_domain},
&rule);
ASSERT_FALSE(AddUrlRule(rule));
Finish();
}
// Ensure patterns with percent encoded hosts match correctly.
//
// Warning: This test depends on the standard non-compliant URL behavior in
// Chrome. Currently, Chrome escapes '*' (%2A) character in URL host, but this
// behavior is non-compliant. See https://crbug.com/1416013 for details. We
// probably no longer need this test once https://crbug.com/1416013 is fixed.
TEST_F(SubresourceFilterIndexedRulesetTest, PercentEncodedHostPattern) {
const char* kPercentEncodedHost = "http://%2A.com/";
ASSERT_TRUE(AddSimpleRule(kPercentEncodedHost));
Finish();
EXPECT_EQ(LoadPolicy::DISALLOW, GetLoadPolicy("http://*.com/"));
EXPECT_EQ(LoadPolicy::DISALLOW, GetLoadPolicy(kPercentEncodedHost));
}
// Verifies the behavior for rules having percent encoded domains.
TEST_F(SubresourceFilterIndexedRulesetTest, PercentEncodedDomain) {
const char* kUrl = "http://example.com";
std::string percent_encoded_host = "%2C.com";
auto rule =
MakeUrlRule(UrlPattern(kUrl, url_pattern_index::testing::kSubstring));
url_pattern_index::testing::AddInitiatorDomains({percent_encoded_host},
&rule);
ASSERT_TRUE(AddUrlRule(rule));
Finish();
// Note: This should actually fail. However url_pattern_index lower cases all
// domains. Hence it doesn't correctly deal with domains having escape
// characters which are percent-encoded in upper case by Chrome's url parser.
EXPECT_EQ(LoadPolicy::ALLOW,
GetLoadPolicy(kUrl, "http://" + percent_encoded_host));
EXPECT_EQ(LoadPolicy::ALLOW, GetLoadPolicy(kUrl, "http://,.com"));
}
TEST_F(SubresourceFilterIndexedRulesetTest, SimpleBlocklistAndAllowlist) {
ASSERT_TRUE(AddSimpleRule("?filter="));
ASSERT_TRUE(AddSimpleAllowlistRule("allowlisted.com/?filter="));
Finish();
EXPECT_EQ(LoadPolicy::DISALLOW,
GetLoadPolicy("http://blocklisted.com?filter=on"));
EXPECT_EQ(LoadPolicy::EXPLICITLY_ALLOW,
GetLoadPolicy("https://allowlisted.com/?filter=on"));
EXPECT_EQ(LoadPolicy::ALLOW, GetLoadPolicy("https://notblocklisted.com"));
}
TEST_F(SubresourceFilterIndexedRulesetTest,
OneBlocklistAndOneDeactivationRule) {
ASSERT_TRUE(AddSimpleRule("example.com"));
ASSERT_TRUE(AddSimpleAllowlistRule("example.com",
url_pattern_index::testing::kDocument));
Finish();
EXPECT_TRUE(ShouldDeactivate("https://example.com", "",
url_pattern_index::testing::kDocument));
EXPECT_FALSE(ShouldDeactivate("https://xample.com", "",
url_pattern_index::testing::kDocument));
EXPECT_EQ(LoadPolicy::DISALLOW, GetLoadPolicy("https://example.com"));
EXPECT_EQ(LoadPolicy::ALLOW, GetLoadPolicy("https://xample.com"));
}
TEST_F(SubresourceFilterIndexedRulesetTest, MatchingEmptyRuleset) {
Finish();
EXPECT_FALSE(MatchingRule(""));
EXPECT_FALSE(MatchingRule("http://example.com"));
EXPECT_FALSE(MatchingRule("http://another.example.com?param=val"));
}
TEST_F(SubresourceFilterIndexedRulesetTest, MatchingNoRuleApplies) {
ASSERT_TRUE(AddSimpleRule("?filter_out="));
ASSERT_TRUE(AddSimpleRule("&filter_out="));
Finish();
EXPECT_FALSE(MatchingRule("http://example.com"));
EXPECT_FALSE(MatchingRule("http://example.com?filter_not"));
}
TEST_F(SubresourceFilterIndexedRulesetTest, MatchingSimpleBlocklist) {
ASSERT_TRUE(AddSimpleRule("?param="));
Finish();
EXPECT_FALSE(MatchingRule("https://example.com"));
EXPECT_TRUE(MatchingRule("http://example.org?param=image1"));
}
TEST_F(SubresourceFilterIndexedRulesetTest, MatchingSimpleAllowlist) {
ASSERT_TRUE(AddSimpleAllowlistRule("example.com/?filter_out="));
Finish();
EXPECT_FALSE(MatchingRule("https://example.com?filter_out=true"));
}
TEST_F(SubresourceFilterIndexedRulesetTest,
MatchingSimpleBlocklistAndAllowlist) {
ASSERT_TRUE(AddSimpleRule("?filter="));
ASSERT_TRUE(AddSimpleAllowlistRule("allowlisted.com/?filter="));
Finish();
EXPECT_TRUE(MatchingRule("http://blocklisted.com?filter=on"));
EXPECT_TRUE(MatchingRule("https://allowlisted.com?filter=on"));
EXPECT_FALSE(MatchingRule("https://notblocklisted.com"));
}
TEST_F(SubresourceFilterIndexedRulesetTest,
MatchingOneBlocklistAndOneDeactivationRule) {
ASSERT_TRUE(AddSimpleRule("example.com"));
ASSERT_TRUE(AddSimpleAllowlistRule("example.com",
url_pattern_index::testing::kDocument));
Finish();
EXPECT_TRUE(MatchingRule("https://example.com"));
EXPECT_FALSE(MatchingRule("https://xample.com"));
}
TEST_F(SubresourceFilterIndexedRulesetTest, RulesetId) {
indexer_ = std::make_unique<RulesetIndexer>(0x12345678);
ASSERT_TRUE(AddSimpleRule("example.com"));
Finish();
EXPECT_EQ(0x12345678u, matcher_->ruleset_id());
}
TEST_F(SubresourceFilterIndexedRulesetTest, StyleRulesSmokeTest) {
AddStyleRule(StyleRuleParams().SetSelector("#ad-1").SetIds({"ad-1"}));
AddStyleRule(StyleRuleParams()
.SetSelector("#ad-2")
.SetDomains({"example.com"})
.SetIds({"ad-2"}));
AddStyleRule(StyleRuleParams().SetSelector(".class1").SetClasses({"class1"}));
Finish();
std::vector<std::string_view> rules;
// Verify domain-specific selector.
matcher_->GetDomainSelectors(url::Origin::Create(GURL("http://example.com")),
rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ("#ad-2", rules[0]);
// Verify lookup by ID.
rules.clear();
matcher_->GetSelectorsById(url::Origin(), "ad-1", GetStyleRuleHash("ad-1"),
rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ("#ad-1", rules[0]);
// Verify lookup by class.
rules.clear();
matcher_->GetSelectorsByClass(url::Origin(), "class1",
GetStyleRuleHash("class1"), rules);
ASSERT_EQ(1u, rules.size());
EXPECT_EQ(".class1", rules[0]);
// Verify MaybeHasStyleRule.
EXPECT_TRUE(matcher_->MaybeHasStyleRule(GetStyleRuleHash("ad-1")));
EXPECT_TRUE(matcher_->MaybeHasStyleRule(GetStyleRuleHash("class1")));
EXPECT_FALSE(matcher_->MaybeHasStyleRule(GetStyleRuleHash("non-existent")));
}
} // namespace subresource_filter