| // 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 |