blob: 9f29f0f1e2baf03631cca926fb432c982de725b3 [file] [log] [blame]
// Copyright 2020 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 "content/browser/webauth/webauth_request_security_checker.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_macros.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/common/origin_util.h"
#include "device/fido/features.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy_feature.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_util.h"
namespace content {
namespace {
constexpr char kCryptotokenOrigin[] =
"chrome-extension://kmendfapggjehodndflmmgagdbamhnfd";
// Returns AuthenticatorStatus::SUCCESS if the domain is valid and an error
// if it fails one of the criteria below.
// Reference https://url.spec.whatwg.org/#valid-domain-string and
// https://html.spec.whatwg.org/multipage/origin.html#concept-origin-effective-domain.
blink::mojom::AuthenticatorStatus ValidateEffectiveDomain(
url::Origin caller_origin) {
// For calls originating in the CryptoToken U2F extension, allow CryptoToken
// to validate domain.
if (WebAuthRequestSecurityChecker::OriginIsCryptoTokenExtension(
caller_origin)) {
return blink::mojom::AuthenticatorStatus::SUCCESS;
}
if (caller_origin.opaque()) {
return blink::mojom::AuthenticatorStatus::OPAQUE_DOMAIN;
}
if (url::HostIsIPAddress(caller_origin.host()) ||
!IsOriginSecure(caller_origin.GetURL())) {
return blink::mojom::AuthenticatorStatus::INVALID_DOMAIN;
}
// Additionally, the scheme is required to be HTTP(S). Other schemes
// may be supported in the future but the webauthn relying party is
// just the domain of the origin so we would have to define how the
// authority part of other schemes maps to a "domain" without
// collisions. Given the |IsOriginSecure| check, just above, HTTP is
// effectively restricted to just "localhost".
if (caller_origin.scheme() != url::kHttpScheme &&
caller_origin.scheme() != url::kHttpsScheme) {
return blink::mojom::AuthenticatorStatus::INVALID_PROTOCOL;
}
return blink::mojom::AuthenticatorStatus::SUCCESS;
}
// Return the relying party ID to use for a request given the requested RP ID
// and the origin of the caller. It's valid for the requested RP ID to be a
// registrable domain suffix of, or be equal to, the origin's effective domain.
// Reference:
// https://html.spec.whatwg.org/multipage/origin.html#is-a-registrable-domain-suffix-of-or-is-equal-to.
base::Optional<std::string> GetRelyingPartyId(
const std::string& claimed_relying_party_id,
const url::Origin& caller_origin) {
if (WebAuthRequestSecurityChecker::OriginIsCryptoTokenExtension(
caller_origin)) {
// This code trusts cryptotoken to handle the validation itself.
return claimed_relying_party_id;
}
if (claimed_relying_party_id.empty()) {
return base::nullopt;
}
if (caller_origin.host() == claimed_relying_party_id) {
return claimed_relying_party_id;
}
if (!caller_origin.DomainIs(claimed_relying_party_id)) {
return base::nullopt;
}
if (!net::registry_controlled_domains::HostHasRegistryControlledDomain(
caller_origin.host(),
net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES) ||
!net::registry_controlled_domains::HostHasRegistryControlledDomain(
claimed_relying_party_id,
net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) {
// TODO(crbug.com/803414): Accept corner-case situations like the following
// origin: "https://login.awesomecompany",
// relying_party_id: "awesomecompany".
return base::nullopt;
}
return claimed_relying_party_id;
}
} // namespace
WebAuthRequestSecurityChecker::WebAuthRequestSecurityChecker(
RenderFrameHost* host)
: render_frame_host_(host) {}
WebAuthRequestSecurityChecker::~WebAuthRequestSecurityChecker() = default;
// static
void WebAuthRequestSecurityChecker::ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure error) {
UMA_HISTOGRAM_ENUMERATION(
"WebAuthentication.RelyingPartySecurityCheckFailure", error);
}
// static
bool WebAuthRequestSecurityChecker::OriginIsCryptoTokenExtension(
const url::Origin& origin) {
auto cryptotoken_origin = url::Origin::Create(GURL(kCryptotokenOrigin));
return cryptotoken_origin == origin;
}
bool WebAuthRequestSecurityChecker::IsSameOriginWithAncestors(
const url::Origin& origin) {
RenderFrameHost* parent = render_frame_host_->GetParent();
while (parent) {
if (!parent->GetLastCommittedOrigin().IsSameOriginWith(origin))
return false;
parent = parent->GetParent();
}
return true;
}
blink::mojom::AuthenticatorStatus
WebAuthRequestSecurityChecker::ValidateAncestorOrigins(
const url::Origin& origin,
bool* is_cross_origin) {
*is_cross_origin = !IsSameOriginWithAncestors(origin);
if ((!base::FeatureList::IsEnabled(device::kWebAuthFeaturePolicy) ||
!static_cast<RenderFrameHostImpl*>(render_frame_host_)
->IsFeatureEnabled(
blink::mojom::FeaturePolicyFeature::kPublicKeyCredentials)) &&
*is_cross_origin) {
ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure::kCrossOriginMismatch);
return blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
}
return blink::mojom::AuthenticatorStatus::SUCCESS;
}
blink::mojom::AuthenticatorStatus
WebAuthRequestSecurityChecker::ValidateDomainAndRelyingPartyID(
const url::Origin& caller_origin,
const std::string& relying_party_id) {
blink::mojom::AuthenticatorStatus domain_validation =
ValidateEffectiveDomain(caller_origin);
if (domain_validation != blink::mojom::AuthenticatorStatus::SUCCESS) {
ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure::kOpaqueOrNonSecureOrigin);
return domain_validation;
}
base::Optional<std::string> valid_rp_id =
GetRelyingPartyId(relying_party_id, caller_origin);
if (!valid_rp_id) {
ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure::kRelyingPartyIdInvalid);
return blink::mojom::AuthenticatorStatus::BAD_RELYING_PARTY_ID;
}
return blink::mojom::AuthenticatorStatus::SUCCESS;
}
blink::mojom::AuthenticatorStatus
WebAuthRequestSecurityChecker::ValidateAPrioriAuthenticatedUrl(
const GURL& url) {
if (url.is_empty())
return blink::mojom::AuthenticatorStatus::SUCCESS;
if (!url.is_valid()) {
ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure::kIconUrlInvalid);
return blink::mojom::AuthenticatorStatus::INVALID_ICON_URL;
}
// https://www.w3.org/TR/mixed-content/#a-priori-authenticated-url
if (!url.IsAboutSrcdoc() && !url.IsAboutBlank() &&
!url.SchemeIs(url::kDataScheme) &&
!network::IsUrlPotentiallyTrustworthy(url)) {
ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure::kIconUrlInvalid);
return blink::mojom::AuthenticatorStatus::INVALID_ICON_URL;
}
return blink::mojom::AuthenticatorStatus::SUCCESS;
}
} // namespace content