blob: 82bffd010433779c15d87dd70d198f003d5b3d56 [file] [log] [blame]
// Copyright 2013 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 "components/password_manager/core/browser/psl_matching_helper.h"
#include <stddef.h>
#include <cctype>
#include "base/macros.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "components/autofill/core/common/password_form.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace password_manager {
namespace {
TEST(PSLMatchingUtilsTest, GetMatchResultNormalCredentials) {
struct TestData {
const char* form_origin;
const char* digest_origin;
MatchResult match_result;
};
const TestData cases[] = {
// Test Exact Matches.
{"http://facebook.com/p", "http://facebook.com/p1",
MatchResult::EXACT_MATCH},
{"http://m.facebook.com/p", "http://m.facebook.com/p1",
MatchResult::EXACT_MATCH},
// Scheme mismatch.
{"http://www.example.com", "https://www.example.com",
MatchResult::NO_MATCH},
// Host mismatch.
{"http://www.example.com", "http://wwwexample.com",
MatchResult::NO_MATCH},
{"http://www.example1.com", "http://www.example2.com",
MatchResult::NO_MATCH},
// Port mismatch.
{"http://www.example.com:123/", "http://www.example.com",
MatchResult::NO_MATCH},
// TLD mismatch.
{"http://www.example.org/", "http://www.example.com/",
MatchResult::NO_MATCH},
// URLs without registry controlled domains should not match.
{"http://localhost/", "http://127.0.0.1/", MatchResult::NO_MATCH},
// Invalid URLs don't match.
{"http://www.example.com/", "http://", MatchResult::NO_MATCH},
{"", "http://www.example.com/", MatchResult::NO_MATCH},
{"http://www.example.com", "bad url", MatchResult::NO_MATCH}};
for (const TestData& data : cases) {
autofill::PasswordForm form;
form.origin = GURL(data.form_origin);
form.signon_realm = form.origin.GetOrigin().spec();
PasswordStore::FormDigest digest(
autofill::PasswordForm::SCHEME_HTML,
GURL(data.digest_origin).GetOrigin().spec(), GURL(data.digest_origin));
EXPECT_EQ(data.match_result, GetMatchResult(form, digest))
<< "form_origin = " << data.form_origin << ", digest = " << digest;
}
}
TEST(PSLMatchingUtilsTest, GetMatchResultPSL) {
struct TestData {
const char* form_origin;
const char* digest_origin;
MatchResult match_result;
};
const TestData cases[] = {
// Test PSL Matches.
{"http://facebook.com/p1", "http://m.facebook.com/p2",
MatchResult::PSL_MATCH},
{"https://www.facebook.com/", "https://m.facebook.com",
MatchResult::PSL_MATCH},
// Don't apply PSL matching to Google domains.
{"https://google.com/", "https://maps.google.com/",
MatchResult::NO_MATCH},
// Scheme mismatch.
{"http://facebook.com/", "https://m.facebook.com/",
MatchResult::NO_MATCH},
{"https://facebook.com/", "http://m.facebook.com/",
MatchResult::NO_MATCH},
// Port mismatch.
{"http://facebook.com/", "https://m.facebook.com:8080/",
MatchResult::NO_MATCH},
{"http://facebook.com:8080/", "https://m.facebook.com/",
MatchResult::NO_MATCH},
// Port match.
{"http://facebook.com:8080/p1", "http://m.facebook.com:8080/p2",
MatchResult::PSL_MATCH},
};
for (const TestData& data : cases) {
autofill::PasswordForm form;
form.origin = GURL(data.form_origin);
form.signon_realm = form.origin.GetOrigin().spec();
PasswordStore::FormDigest digest(
autofill::PasswordForm::SCHEME_HTML,
GURL(data.digest_origin).GetOrigin().spec(), GURL(data.digest_origin));
EXPECT_EQ(data.match_result, GetMatchResult(form, digest))
<< "form_origin = " << data.form_origin << ", digest = " << digest;
}
}
TEST(PSLMatchingUtilsTest, GetMatchResultFederated) {
struct TestData {
const char* form_origin;
const char* form_federation_origin;
const char* digest_origin;
MatchResult match_result;
};
const TestData cases[] = {
// Test Federated Matches.
{"https://example.com/p", "https://google.com", "https://example.com/p2",
MatchResult::FEDERATED_MATCH},
// Empty federation providers don't match.
{"https://example.com/", "", "https://example.com",
MatchResult::NO_MATCH},
// Invalid origins don't match.
{"https://example.com/", "https://google.com", "example.com",
MatchResult::NO_MATCH},
{"https://example.com/", "https://google.com", "example",
MatchResult::NO_MATCH},
// TLD Mismatch.
{"https://example.com/", "https://google.com", "https://example.org",
MatchResult::NO_MATCH},
// Scheme mismatch.
{"http://example.com/", "https://google.com", "https://example.com/",
MatchResult::NO_MATCH},
{"https://example.com/", "https://google.com", "http://example.com/",
MatchResult::NO_MATCH},
// Port mismatch.
{"https://localhost/", "https://google.com", "http://localhost:8080",
MatchResult::NO_MATCH},
{"https://localhost:8080", "https://google.com", "http://localhost",
MatchResult::NO_MATCH},
// Port match.
{"https://localhost:8080/p", "https://google.com",
"https://localhost:8080/p2", MatchResult::FEDERATED_MATCH},
};
for (const TestData& data : cases) {
autofill::PasswordForm form;
form.origin = GURL(data.form_origin);
form.federation_origin =
url::Origin::Create(GURL(data.form_federation_origin));
form.signon_realm = "federation://" + form.origin.host() + "/" +
form.federation_origin.host();
PasswordStore::FormDigest digest(
autofill::PasswordForm::SCHEME_HTML,
GURL(data.digest_origin).GetOrigin().spec(), GURL(data.digest_origin));
EXPECT_EQ(data.match_result, GetMatchResult(form, digest))
<< "form_origin = " << data.form_origin
<< ", form_federation_origin = " << data.form_federation_origin
<< ", digest = " << digest;
}
}
TEST(PSLMatchingUtilsTest, GetMatchResultFederatedPSL) {
struct TestData {
const char* form_origin;
const char* form_federation_origin;
const char* digest_origin;
MatchResult match_result;
};
const TestData cases[] = {
// Test Federated PSL Matches.
{"https://sub.example.com/p2", "https://google.com",
"https://sub.example.com/p1", MatchResult::FEDERATED_MATCH},
{"https://sub1.example.com/p1", "https://google.com",
"https://sub2.example.com/p2", MatchResult::FEDERATED_PSL_MATCH},
{"https://example.com/", "https://google.com", "https://sub.example.com",
MatchResult::FEDERATED_PSL_MATCH},
// Federated PSL matches do not apply to HTTP.
{"https://sub1.example.com/", "https://google.com",
"http://sub2.example.com", MatchResult::NO_MATCH},
{"https://example.com/", "https://google.com", "http://sub.example.com",
MatchResult::NO_MATCH},
{"http://example.com/", "https://google.com", "https://example.com/",
MatchResult::NO_MATCH},
// Federated PSL matches do not apply to Google on HTTP or HTTPS.
{"https://accounts.google.com/", "https://facebook.com",
"https://maps.google.com", MatchResult::NO_MATCH},
{"https://accounts.google.com/facebook.com", "https://facebook.com",
"http://maps.google.com", MatchResult::NO_MATCH},
// TLD Mismatch.
{"https://sub.example.com/google.com", "https://google.com",
"https://sub.example.org", MatchResult::NO_MATCH},
// Port mismatch.
{"https://example.com/", "https://google.com", "https://example.com:8080",
MatchResult::NO_MATCH},
{"https://example.com:8080", "https://google.com", "https://example.com",
MatchResult::NO_MATCH},
// Port match.
{"https://sub.example.com:8080/p", "https://google.com",
"https://sub2.example.com:8080/p2", MatchResult::FEDERATED_PSL_MATCH},
};
for (const TestData& data : cases) {
autofill::PasswordForm form;
form.origin = GURL(data.form_origin);
form.federation_origin =
url::Origin::Create(GURL(data.form_federation_origin));
form.signon_realm = "federation://" + form.origin.host() + "/" +
form.federation_origin.host();
PasswordStore::FormDigest digest(
autofill::PasswordForm::SCHEME_HTML,
GURL(data.digest_origin).GetOrigin().spec(), GURL(data.digest_origin));
EXPECT_EQ(data.match_result, GetMatchResult(form, digest))
<< "form_origin = " << data.form_origin
<< ", form_federation_origin = " << data.form_federation_origin
<< ", digest = " << digest;
}
}
TEST(PSLMatchingUtilsTest, IsPublicSuffixDomainMatch) {
struct TestPair {
const char* url1;
const char* url2;
bool should_match;
};
const TestPair pairs[] = {
{"http://facebook.com", "http://facebook.com", true},
{"http://facebook.com/path", "http://facebook.com/path", true},
{"http://facebook.com/path1", "http://facebook.com/path2", true},
{"http://facebook.com", "http://m.facebook.com", true},
{"http://www.facebook.com", "http://m.facebook.com", true},
{"http://facebook.com/path", "http://m.facebook.com/path", true},
{"http://facebook.com/path1", "http://m.facebook.com/path2", true},
{"http://example.com/has space", "http://example.com/has space", true},
{"http://www.example.com", "http://wwwexample.com", false},
{"http://www.example.com", "https://www.example.com", false},
{"http://www.example.com:123", "http://www.example.com", false},
{"http://www.example.org", "http://www.example.com", false},
// URLs without registry controlled domains should not match.
{"http://localhost", "http://127.0.0.1", false},
// Invalid URLs should not match anything.
{"http://", "http://", false},
{"", "", false},
{"bad url", "bad url", false},
{"http://www.example.com", "http://", false},
{"", "http://www.example.com", false},
{"http://www.example.com", "bad url", false},
{"http://www.example.com/%00", "http://www.example.com/%00", false},
{"federation://example.com/google.com", "https://example.com/", false},
};
for (const TestPair& pair : pairs) {
autofill::PasswordForm form1;
form1.signon_realm = pair.url1;
autofill::PasswordForm form2;
form2.signon_realm = pair.url2;
EXPECT_EQ(pair.should_match,
IsPublicSuffixDomainMatch(form1.signon_realm, form2.signon_realm))
<< "First URL = " << pair.url1 << ", second URL = " << pair.url2;
}
}
TEST(PSLMatchingUtilsTest, IsFederatedRealm) {
struct TestPair {
const char* form_signon_realm;
const char* origin;
bool should_match;
};
const TestPair pairs[] = {
{"https://facebook.com", "https://facebook.com", false},
{"", "", false},
{"", "https://facebook.com/", false},
{"https://facebook.com/", "", false},
{"federation://example.com/google.com", "https://example.com/", true},
{"federation://example.com/google.com", "http://example.com/", true},
{"federation://example.com/google.com", "example.com", false},
{"federation://example.com/", "http://example.com/", false},
{"federation://example.com/google.com", "example", false},
{"federation://localhost/google.com", "http://localhost/", true},
{"federation://localhost/google.com", "http://localhost:8000/", true},
};
for (const TestPair& pair : pairs) {
EXPECT_EQ(pair.should_match,
IsFederatedRealm(pair.form_signon_realm, GURL(pair.origin)))
<< "form_signon_realm = " << pair.form_signon_realm
<< ", origin = " << pair.origin;
}
}
TEST(PSLMatchingUtilsTest, IsFederatedPSLMatch) {
struct TestPair {
const char* form_signon_realm;
const char* form_origin;
const char* origin;
bool should_match;
};
const TestPair pairs[] = {
{"https://facebook.com", "https://facebook.com", "https://facebook.com",
false},
{"", "", "", false},
{"", "", "https://facebook.com/", false},
{"https://facebook.com/", "https://facebook.com/", "", false},
{"federation://example.com/google.com", "https://example.com/p1",
"https://example.com/p2", true},
{"federation://example.com/google.com", "https://example.com/",
"http://example.com/", false},
{"federation://example.com/google.com", "https://example.com/",
"example.com", false},
{"federation://example.com/", "https://example.com/",
"https://example.com/", false},
{"federation://example.com/google.com", "https://example.com/", "example",
false},
{"federation://sub.example.com/google.com", "https://sub.example.com/p1",
"https://sub.example.com/p2", true},
{"federation://sub.example.com/google.com", "https://sub.example.com/p1",
"https://sub2.example.com/p2", true},
{"federation://example.com/google.com", "https://example.com/p1",
"https://sub.example.com/", true},
{"federation://example.com/google.com", "https://example.com/",
"http://sub.example.com/", false},
{"federation://sub.example.com/", "https://sub.example.com/",
"https://sub.example.com/", false},
{"federation://localhost/google.com", "http://localhost/",
"http://localhost/", true},
{"federation://localhost/google.com", "http://localhost:8000/",
"http://localhost:8000/", true},
{"federation://localhost/google.com", "http://localhost:8000/",
"http://localhost/", false},
{"federation://localhost/google.com", "http://localhost/",
"http://localhost:8000/", false},
};
for (const TestPair& pair : pairs) {
EXPECT_EQ(pair.should_match,
IsFederatedPSLMatch(pair.form_signon_realm,
GURL(pair.form_origin), GURL(pair.origin)))
<< "form_signon_realm = " << pair.form_signon_realm
<< "form_origin = " << pair.form_origin << ", origin = " << pair.origin;
}
}
TEST(PSLMatchingUtilsTest, GetOrganizationIdentifyingName) {
static constexpr const struct {
const char* url;
const char* expected_organization_name;
} kTestCases[] = {
{"http://example.com/login", "example"},
{"https://example.com", "example"},
{"ftp://example.com/ftp_realm", "example"},
{"http://foo.bar.example.com", "example"},
{"http://example.co.uk", "example"},
{"http://bar.example.appspot.com", "example"},
{"http://foo.bar", "foo"},
{"https://user:pass@www.example.com:80/path?query#ref", "example"},
{"http://www.foo+bar.com", "foo+bar"},
{"http://www.foo-bar.com", "foo-bar"},
{"https://foo_bar.com", "foo_bar"},
{"http://www.foo%2Bbar.com", "foo+bar"},
{"http://www.foo%2Dbar.com", "foo-bar"},
{"https://foo%5Fbar.com", "foo_bar"},
{"http://www.foo%2Ebar.com", "bar"},
// Internationalized Domain Names: each dot-separated label of the domain
// name is individually Punycode-encoded, so the organization-identifying
// name is still well-defined and can be determined as normal.
// , ,
// szotar = sz\xc3\xb3t\xc3\xa1r (UTF-8) = xn--sztr-7na0i (IDN)
// | |
// U+00E1 U+00F3
{"https://www.sz\xc3\xb3t\xc3\xa1r.appspot.com", "xn--sztr-7na0i"},
{"http://www.foo!bar.com", "foo%21bar"},
{"http://www.foo%21bar.com", "foo%21bar"},
{"http://www.foo$bar.com", "foo%24bar"},
{"http://www.foo&bar.com", "foo%26bar"},
{"http://www.foo\'bar.com", "foo%27bar"},
{"http://www.foo(bar.com", "foo%28bar"},
{"http://www.foo)bar.com", "foo%29bar"},
{"http://www.foo*bar.com", "foo%2Abar"},
{"http://www.foo,bar.com", "foo%2Cbar"},
{"http://www.foo=bar.com", "foo%3Dbar"},
// URLs without host portions, hosts without registry controlled domains
// should, or hosts consisting of a registry yield the empty string.
{"http://localhost", ""},
{"http://co.uk", ""},
{"http://google", ""},
{"http://127.0.0.1", ""},
{"file:///usr/bin/stuff", ""},
{"federation://example.com/google.com", ""},
{"android://hash@com.example/", ""},
{"http://[1080:0:0:0:8:800:200C:417A]/", ""},
{"http://[3ffe:2a00:100:7031::1]", ""},
{"http://[::192.9.5.5]/", ""},
// Invalid URLs should yield the empty string.
{"", ""},
{"http://", ""},
{"bad url", ""},
{"http://www.example.com/%00", ""},
{"http://www.foo;bar.com", ""},
{"http://www.foo~bar.com", ""},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.url);
GURL url(test_case.url);
EXPECT_EQ(test_case.expected_organization_name,
GetOrganizationIdentifyingName(url));
}
}
// Apart from alphanumeric characters and '.', only |kExpectedUnescapedChars|
// are expected to appear without percent-encoding in the domain of a valid,
// canonicalized URL.
//
// The purpose of this test is to ensure that the test cases around unescaped
// special characters in `GetOrganizationIdentifyingName` are exhaustive.
TEST(PSLMatchingUtilsTest,
GetOrganizationIdentifyingName_UnescapedSpecialChars) {
static constexpr const char kExpectedNonAlnumChars[] = {'+', '-', '_', '.'};
for (int chr = 0; chr <= 255; ++chr) {
const auto percent_encoded = base::StringPrintf("http://a%%%02Xb.hu/", chr);
const GURL url(percent_encoded);
if (isalnum(chr) || base::ContainsValue(kExpectedNonAlnumChars, chr)) {
ASSERT_TRUE(url.is_valid());
const auto percent_decoded = base::StringPrintf(
"http://a%cb.hu/", base::ToLowerASCII(static_cast<char>(chr)));
EXPECT_EQ(percent_decoded, url.spec());
} else if (url.is_valid()) {
EXPECT_EQ(percent_encoded, url.spec());
}
}
}
} // namespace
} // namespace password_manager