blob: e458082a84f8d1cfd7b8ddb43955af07cc92d9e5 [file] [log] [blame]
// Copyright 2014 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 "third_party/blink/renderer/core/frame/csp/csp_source.h"
#include "third_party/blink/public/platform/web_content_security_policy_struct.h"
#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include "third_party/blink/renderer/core/frame/use_counter.h"
#include "third_party/blink/renderer/platform/weborigin/known_ports.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/assertions.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace blink {
CSPSource::CSPSource(ContentSecurityPolicy* policy,
const String& scheme,
const String& host,
int port,
const String& path,
WildcardDisposition host_wildcard,
WildcardDisposition port_wildcard)
: policy_(policy),
scheme_(scheme.DeprecatedLower()),
host_(host),
port_(port),
path_(path),
host_wildcard_(host_wildcard),
port_wildcard_(port_wildcard) {}
bool CSPSource::Matches(const KURL& url,
ResourceRequest::RedirectStatus redirect_status) const {
SchemeMatchingResult schemes_match = SchemeMatches(url.Protocol());
if (schemes_match == SchemeMatchingResult::kNotMatching)
return false;
if (IsSchemeOnly())
return true;
bool paths_match = (redirect_status == RedirectStatus::kFollowedRedirect) ||
PathMatches(url.GetPath());
PortMatchingResult ports_match = PortMatches(url.Port(), url.Protocol());
// if either the scheme or the port would require an upgrade (e.g. from http
// to https) then check that both of them can upgrade to ensure that we don't
// run into situations where we only upgrade the port but not the scheme or
// viceversa
if ((RequiresUpgrade(schemes_match) || (RequiresUpgrade(ports_match))) &&
(!CanUpgrade(schemes_match) || !CanUpgrade(ports_match))) {
return false;
}
return HostMatches(url.Host()) &&
ports_match != PortMatchingResult::kNotMatching && paths_match;
}
bool CSPSource::MatchesAsSelf(const KURL& url) {
// https://w3c.github.io/webappsec-csp/#match-url-to-source-expression
// Step 4.
SchemeMatchingResult schemes_match = SchemeMatches(url.Protocol());
bool hosts_match = HostMatches(url.Host());
PortMatchingResult ports_match = PortMatches(url.Port(), url.Protocol());
// check if the origin is exactly matching
if (schemes_match == SchemeMatchingResult::kMatchingExact && hosts_match &&
(ports_match == PortMatchingResult::kMatchingExact ||
ports_match == PortMatchingResult::kMatchingWildcard)) {
return true;
}
String self_scheme =
(scheme_.IsEmpty() ? policy_->GetSelfProtocol() : scheme_);
bool ports_match_or_defaults =
(ports_match == PortMatchingResult::kMatchingExact ||
((IsDefaultPortForProtocol(port_, self_scheme) || port_ == 0) &&
(IsDefaultPortForProtocol(url.Port(), url.Protocol()) ||
url.Port() == 0)));
if (hosts_match && ports_match_or_defaults &&
(url.Protocol() == "https" || url.Protocol() == "wss" ||
self_scheme == "http")) {
return true;
}
return false;
}
CSPSource::SchemeMatchingResult CSPSource::SchemeMatches(
const String& protocol) const {
DCHECK_EQ(protocol, protocol.DeprecatedLower());
const String& scheme =
(scheme_.IsEmpty() ? policy_->GetSelfProtocol() : scheme_);
if (scheme == protocol)
return SchemeMatchingResult::kMatchingExact;
if ((scheme == "http" && protocol == "https") ||
(scheme == "ws" && protocol == "wss")) {
return SchemeMatchingResult::kMatchingUpgrade;
}
return SchemeMatchingResult::kNotMatching;
}
bool CSPSource::HostMatches(const String& host) const {
Document* document = policy_->GetDocument();
bool match;
bool equal_hosts = host_ == host;
if (host_wildcard_ == kHasWildcard) {
if (host_.IsEmpty()) {
// host-part = "*"
match = true;
} else {
// host-part = "*." 1*host-char *( "." 1*host-char )
match = host.EndsWithIgnoringCase(String("." + host_));
}
// Chrome used to, incorrectly, match *.x.y to x.y. This was fixed, but
// the following count measures when a match fails that would have
// passed the old, incorrect style, in case a lot of sites were
// relying on that behavior.
if (document && equal_hosts) {
UseCounter::Count(*document,
WebFeature::kCSPSourceWildcardWouldMatchExactHost);
}
} else {
// host-part = 1*host-char *( "." 1*host-char )
match = equal_hosts;
}
return match;
}
bool CSPSource::PathMatches(const String& url_path) const {
if (path_.IsEmpty() || (path_ == "/" && url_path.IsEmpty()))
return true;
String path = DecodeURLEscapeSequences(url_path);
if (path_.EndsWith("/"))
return path.StartsWith(path_);
return path == path_;
}
CSPSource::PortMatchingResult CSPSource::PortMatches(
int port,
const String& protocol) const {
if (port_wildcard_ == kHasWildcard)
return PortMatchingResult::kMatchingWildcard;
if (port == port_) {
if (port == 0)
return PortMatchingResult::kMatchingWildcard;
return PortMatchingResult::kMatchingExact;
}
bool is_scheme_http; // needed for detecting an upgrade when the port is 0
is_scheme_http = scheme_.IsEmpty() ? policy_->ProtocolEqualsSelf("http")
: EqualIgnoringASCIICase("http", scheme_);
if ((port_ == 80 || (port_ == 0 && is_scheme_http)) &&
(port == 443 || (port == 0 && DefaultPortForProtocol(protocol) == 443)))
return PortMatchingResult::kMatchingUpgrade;
if (!port) {
if (IsDefaultPortForProtocol(port_, protocol))
return PortMatchingResult::kMatchingExact;
return PortMatchingResult::kNotMatching;
}
if (!port_) {
if (IsDefaultPortForProtocol(port, protocol))
return PortMatchingResult::kMatchingExact;
return PortMatchingResult::kNotMatching;
}
return PortMatchingResult::kNotMatching;
}
bool CSPSource::Subsumes(CSPSource* other) const {
if (SchemeMatches(other->scheme_) == SchemeMatchingResult::kNotMatching)
return false;
if (other->IsSchemeOnly() || IsSchemeOnly())
return IsSchemeOnly();
if ((host_wildcard_ == kNoWildcard &&
other->host_wildcard_ == kHasWildcard) ||
(port_wildcard_ == kNoWildcard &&
other->port_wildcard_ == kHasWildcard)) {
return false;
}
bool host_subsumes = (host_ == other->host_ || HostMatches(other->host_));
bool port_subsumes = (port_wildcard_ == kHasWildcard) ||
PortMatches(other->port_, other->scheme_) !=
PortMatchingResult::kNotMatching;
bool path_subsumes = PathMatches(other->path_);
return host_subsumes && port_subsumes && path_subsumes;
}
bool CSPSource::IsSimilar(CSPSource* other) const {
bool schemes_match =
SchemeMatches(other->scheme_) != SchemeMatchingResult::kNotMatching ||
other->SchemeMatches(scheme_) != SchemeMatchingResult::kNotMatching;
if (!schemes_match || IsSchemeOnly() || other->IsSchemeOnly())
return schemes_match;
bool hosts_match = (host_ == other->host_) || HostMatches(other->host_) ||
other->HostMatches(host_);
bool ports_match =
(other->port_wildcard_ == kHasWildcard) ||
PortMatches(other->port_, other->scheme_) !=
PortMatchingResult::kNotMatching ||
other->PortMatches(port_, scheme_) != PortMatchingResult::kNotMatching;
bool paths_match = PathMatches(other->path_) || other->PathMatches(path_);
if (hosts_match && ports_match && paths_match)
return true;
return false;
}
CSPSource* CSPSource::Intersect(CSPSource* other) const {
if (!IsSimilar(other))
return nullptr;
String scheme =
other->SchemeMatches(scheme_) != SchemeMatchingResult::kNotMatching
? scheme_
: other->scheme_;
if (IsSchemeOnly() || other->IsSchemeOnly()) {
const CSPSource* stricter = IsSchemeOnly() ? other : this;
return new CSPSource(policy_, scheme, stricter->host_, stricter->port_,
stricter->path_, stricter->host_wildcard_,
stricter->port_wildcard_);
}
String host = host_wildcard_ == kNoWildcard ? host_ : other->host_;
// Since sources are similar and paths match, pick the longer one.
String path = path_.length() > other->path_.length() ? path_ : other->path_;
// Choose this port if the other port is empty, has wildcard or is a port for
// a less secure scheme such as "http" whereas scheme of this is "https", in
// which case the lengths would differ.
int port = (other->port_wildcard_ == kHasWildcard || !other->port_ ||
scheme_.length() > other->scheme_.length())
? port_
: other->port_;
WildcardDisposition host_wildcard =
(host_wildcard_ == kHasWildcard) ? other->host_wildcard_ : host_wildcard_;
WildcardDisposition port_wildcard =
(port_wildcard_ == kHasWildcard) ? other->port_wildcard_ : port_wildcard_;
return new CSPSource(policy_, scheme, host, port, path, host_wildcard,
port_wildcard);
}
bool CSPSource::IsSchemeOnly() const {
return host_.IsEmpty() && (host_wildcard_ == kNoWildcard);
}
bool CSPSource::FirstSubsumesSecond(
const HeapVector<Member<CSPSource>>& list_a,
const HeapVector<Member<CSPSource>>& list_b) {
// Empty vector of CSPSources has an effect of 'none'.
if (!list_a.size() || !list_b.size())
return !list_b.size();
// Walk through all the items in |listB|, ensuring that each is subsumed by at
// least one item in |listA|. If any item in |listB| is not subsumed, return
// false.
for (const auto& source_b : list_b) {
bool found_match = false;
for (const auto& source_a : list_a) {
if ((found_match = source_a->Subsumes(source_b)))
break;
}
if (!found_match)
return false;
}
return true;
}
WebContentSecurityPolicySourceExpression
CSPSource::ExposeForNavigationalChecks() const {
WebContentSecurityPolicySourceExpression source_expression;
source_expression.scheme = scheme_;
source_expression.host = host_;
source_expression.is_host_wildcard =
static_cast<WebWildcardDisposition>(host_wildcard_);
source_expression.port = port_;
source_expression.is_port_wildcard =
static_cast<WebWildcardDisposition>(port_wildcard_);
source_expression.path = path_;
return source_expression;
}
void CSPSource::Trace(blink::Visitor* visitor) {
visitor->Trace(policy_);
}
STATIC_ASSERT_ENUM(kWebWildcardDispositionNoWildcard, CSPSource::kNoWildcard);
STATIC_ASSERT_ENUM(kWebWildcardDispositionHasWildcard, CSPSource::kHasWildcard);
} // namespace blink