Commit 41c5a44c authored by Mike West's avatar Mike West Committed by Commit Bot

Add a UseCounter for cross-origin accesses requiring 'document.domain'.

Bug: 787905
Change-Id: I2248173947ed87ff3e8f40dc86a8e9044ed2dd3b
Reviewed-on: https://chromium-review.googlesource.com/966442
Commit-Queue: Mike West <mkwst@chromium.org>
Reviewed-by: default avatarYuki Shiino <yukishiino@chromium.org>
Cr-Commit-Position: refs/heads/master@{#589470}
parent 7d691a6b
......@@ -1995,6 +1995,8 @@ enum WebFeature {
kTextDecoderStreamConstructor = 2540,
kSignedExchangeInnerResponse = 2541,
kPaymentAddressLanguageCode = 2542,
kDocumentDomainBlockedCrossOriginAccess = 2543,
kDocumentDomainEnabledCrossOriginAccess = 2544,
// Add new features immediately above this line. Don't change assigned
// numbers of any item, and don't reuse removed slots.
......
......@@ -65,10 +65,22 @@ bool CanAccessWindowInternal(const LocalDOMWindow* accessing_window,
const SecurityOrigin* accessing_origin =
accessing_window->document()->GetSecurityOrigin();
const LocalDOMWindow* local_target_window = ToLocalDOMWindow(target_window);
if (!accessing_origin->CanAccess(
local_target_window->document()->GetSecurityOrigin())) {
return false;
SecurityOrigin::AccessResultDomainDetail detail;
bool can_access = accessing_origin->CanAccess(
local_target_window->document()->GetSecurityOrigin(), detail);
if (detail ==
SecurityOrigin::AccessResultDomainDetail::kDomainSetByOnlyOneOrigin ||
detail ==
SecurityOrigin::AccessResultDomainDetail::kDomainMatchNecessary ||
detail == SecurityOrigin::AccessResultDomainDetail::kDomainMismatch) {
UseCounter::Count(
accessing_window->GetFrame(),
can_access ? WebFeature::kDocumentDomainEnabledCrossOriginAccess
: WebFeature::kDocumentDomainBlockedCrossOriginAccess);
}
if (!can_access)
return false;
// Notify the loader's client if the initial document has been accessed.
LocalFrame* target_frame = local_target_window->GetFrame();
......
......@@ -17,56 +17,97 @@ namespace blink {
namespace {
const char kMainFrame[] = "https://example.com/main.html";
const char kSameOriginTarget[] = "https://example.com/target.html";
const char kSameOriginDomainTarget[] = "https://sub.example.com/target.html";
const char kCrossOriginTarget[] = "https://not-example.com/target.html";
const char kTargetHTML[] =
"<!DOCTYPE html>"
"<script>"
" (window.opener || window.top).postMessage('yay', '*');"
"</script>";
const char kSameOriginDomainTargetHTML[] =
"<!DOCTYPE html>"
"<script>"
" document.domain = 'example.com';"
" (window.opener || window.top).postMessage('yay', '*');"
"</script>";
}
class BindingSecurityCounterTest
: public SimTest,
public testing::WithParamInterface<const char*> {
public:
enum class OriginDisposition { CrossOrigin, SameOrigin };
enum class OriginDisposition { CrossOrigin, SameOrigin, SameOriginDomain };
BindingSecurityCounterTest() = default;
void LoadWindowAndAccessProperty(OriginDisposition which_origin,
const String& property) {
const char* target_url;
const char* target_html;
switch (which_origin) {
case OriginDisposition::CrossOrigin:
target_url = kCrossOriginTarget;
target_html = kTargetHTML;
break;
case OriginDisposition::SameOrigin:
target_url = kSameOriginTarget;
target_html = kTargetHTML;
break;
case OriginDisposition::SameOriginDomain:
target_url = kSameOriginDomainTarget;
target_html = kSameOriginDomainTargetHTML;
break;
}
SimRequest main(kMainFrame, "text/html");
SimRequest target(which_origin == OriginDisposition::CrossOrigin
? kCrossOriginTarget
: kSameOriginTarget,
"text/html");
SimRequest target(target_url, "text/html");
const String& document = String::Format(
"<!DOCTYPE html>"
"<script>"
" %s"
" window.addEventListener('message', e => {"
" window.other = e.source.%s;"
" console.log('yay');"
" });"
" var w = window.open('%s');"
"</script>",
property.Utf8().data(),
which_origin == OriginDisposition::CrossOrigin ? kCrossOriginTarget
: kSameOriginTarget);
which_origin == OriginDisposition::SameOriginDomain
? "document.domain = 'example.com';"
: "",
property.Utf8().data(), target_url);
LoadURL(kMainFrame);
main.Complete(document);
target.Complete(
"<!DOCTYPE html>"
"<script>window.opener.postMessage('yay', '*');</script>");
target.Complete(target_html);
test::RunPendingTasks();
}
void LoadFrameAndAccessProperty(OriginDisposition which_origin,
const String& property) {
const char* target_url;
const char* target_html;
switch (which_origin) {
case OriginDisposition::CrossOrigin:
target_url = kCrossOriginTarget;
target_html = kTargetHTML;
break;
case OriginDisposition::SameOrigin:
target_url = kSameOriginTarget;
target_html = kTargetHTML;
break;
case OriginDisposition::SameOriginDomain:
target_url = kSameOriginDomainTarget;
target_html = kSameOriginDomainTargetHTML;
break;
}
SimRequest main(kMainFrame, "text/html");
SimRequest target(which_origin == OriginDisposition::CrossOrigin
? kCrossOriginTarget
: kSameOriginTarget,
"text/html");
SimRequest target(target_url, "text/html");
const String& document = String::Format(
"<!DOCTYPE html>"
"<body>"
"<script>"
" %s"
" var i = document.createElement('iframe');"
" window.addEventListener('message', e => {"
" window.other = e.source.%s;"
......@@ -75,15 +116,14 @@ class BindingSecurityCounterTest
" i.src = '%s';"
" document.body.appendChild(i);"
"</script>",
property.Utf8().data(),
which_origin == OriginDisposition::CrossOrigin ? kCrossOriginTarget
: kSameOriginTarget);
which_origin == OriginDisposition::SameOriginDomain
? "document.domain = 'example.com';"
: "",
property.Utf8().data(), target_url);
LoadURL(kMainFrame);
main.Complete(document);
target.Complete(
"<!DOCTYPE html>"
"<script>window.top.postMessage('yay', '*');</script>");
target.Complete(target_html);
test::RunPendingTasks();
}
};
......@@ -110,6 +150,8 @@ TEST_P(BindingSecurityCounterTest, CrossOriginWindow) {
WebFeature::kCrossOriginPropertyAccess));
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccessFromOpener));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kDocumentDomainEnabledCrossOriginAccess));
}
TEST_P(BindingSecurityCounterTest, SameOriginWindow) {
......@@ -118,6 +160,18 @@ TEST_P(BindingSecurityCounterTest, SameOriginWindow) {
WebFeature::kCrossOriginPropertyAccess));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccessFromOpener));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kDocumentDomainEnabledCrossOriginAccess));
}
TEST_P(BindingSecurityCounterTest, SameOriginDomainWindow) {
LoadWindowAndAccessProperty(OriginDisposition::SameOriginDomain, GetParam());
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccess));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccessFromOpener));
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kDocumentDomainEnabledCrossOriginAccess));
}
TEST_P(BindingSecurityCounterTest, CrossOriginFrame) {
......@@ -126,6 +180,8 @@ TEST_P(BindingSecurityCounterTest, CrossOriginFrame) {
WebFeature::kCrossOriginPropertyAccess));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccessFromOpener));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kDocumentDomainEnabledCrossOriginAccess));
}
TEST_P(BindingSecurityCounterTest, SameOriginFrame) {
......@@ -134,6 +190,18 @@ TEST_P(BindingSecurityCounterTest, SameOriginFrame) {
WebFeature::kCrossOriginPropertyAccess));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccessFromOpener));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kDocumentDomainEnabledCrossOriginAccess));
}
TEST_P(BindingSecurityCounterTest, SameOriginDomainFrame) {
LoadFrameAndAccessProperty(OriginDisposition::SameOriginDomain, GetParam());
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccess));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccessFromOpener));
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kDocumentDomainEnabledCrossOriginAccess));
}
} // namespace
......@@ -244,15 +244,22 @@ bool SecurityOrigin::SerializesAsNull() const {
return false;
}
bool SecurityOrigin::CanAccess(const SecurityOrigin* other) const {
if (universal_access_)
bool SecurityOrigin::CanAccess(const SecurityOrigin* other,
AccessResultDomainDetail& detail) const {
if (universal_access_) {
detail = AccessResultDomainDetail::kDomainNotRelevant;
return true;
}
if (this == other)
if (this == other) {
detail = AccessResultDomainDetail::kDomainNotRelevant;
return true;
}
if (IsOpaque() || other->IsOpaque())
if (IsOpaque() || other->IsOpaque()) {
detail = AccessResultDomainDetail::kDomainNotRelevant;
return false;
}
// document.domain handling, as per
// https://html.spec.whatwg.org/multipage/browsers.html#dom-document-domain:
......@@ -266,6 +273,7 @@ bool SecurityOrigin::CanAccess(const SecurityOrigin* other) const {
bool can_access = false;
if (protocol_ == other->protocol_) {
if (!domain_was_set_in_dom_ && !other->domain_was_set_in_dom_) {
detail = AccessResultDomainDetail::kDomainNotSet;
if (host_ == other->host_ && port_ == other->port_)
can_access = true;
} else if (domain_was_set_in_dom_ && other->domain_was_set_in_dom_) {
......@@ -274,12 +282,25 @@ bool SecurityOrigin::CanAccess(const SecurityOrigin* other) const {
// https://crbug.com/733150
if (domain_ == other->domain_ && domain_ != "null") {
can_access = true;
detail = (host_ == other->host_ && port_ == other->port_)
? AccessResultDomainDetail::kDomainMatchUnnecessary
: AccessResultDomainDetail::kDomainMatchNecessary;
} else {
detail = (host_ == other->host_ && port_ == other->port_)
? AccessResultDomainDetail::kDomainMismatch
: AccessResultDomainDetail::kDomainNotRelevant;
}
} else {
detail = (host_ == other->host_ && port_ == other->port_)
? AccessResultDomainDetail::kDomainSetByOnlyOneOrigin
: AccessResultDomainDetail::kDomainNotRelevant;
}
}
if (can_access && IsLocal())
can_access = PassesFileCheck(other);
if (can_access && IsLocal() && !PassesFileCheck(other)) {
detail = AccessResultDomainDetail::kDomainNotRelevant;
can_access = false;
}
return can_access;
}
......
......@@ -55,6 +55,15 @@ class PLATFORM_EXPORT SecurityOrigin : public RefCounted<SecurityOrigin> {
WTF_MAKE_NONCOPYABLE(SecurityOrigin);
public:
enum class AccessResultDomainDetail {
kDomainNotRelevant,
kDomainNotSet,
kDomainSetByOnlyOneOrigin,
kDomainMatchNecessary,
kDomainMatchUnnecessary,
kDomainMismatch,
};
static scoped_refptr<SecurityOrigin> Create(const KURL&);
// Creates a new opaque SecurityOrigin that is guaranteed to be cross-origin
// to all currently existing SecurityOrigins.
......@@ -112,7 +121,17 @@ class PLATFORM_EXPORT SecurityOrigin : public RefCounted<SecurityOrigin> {
// SecurityOrigin. For example, call this function before allowing
// script from one security origin to read or write objects from
// another SecurityOrigin.
bool CanAccess(const SecurityOrigin*) const;
bool CanAccess(const SecurityOrigin* other) const {
AccessResultDomainDetail unused_detail;
return CanAccess(other, unused_detail);
}
// Returns true if this SecurityOrigin can script objects in |other|, just
// as above, but also returns the category into which the access check fell.
//
// TODO(crbug.com/787905): Remove this variant once we have enough data to
// make decisions about `document.domain`.
bool CanAccess(const SecurityOrigin* other, AccessResultDomainDetail&) const;
// Returns true if this SecurityOrigin can read content retrieved from
// the given URL.
......
......@@ -247,6 +247,79 @@ TEST_F(SecurityOriginTest, CanAccess) {
}
}
TEST_F(SecurityOriginTest, CanAccessDetail) {
struct TestCase {
SecurityOrigin::AccessResultDomainDetail expected;
const char* origin1;
const char* domain1;
const char* origin2;
const char* domain2;
};
TestCase tests[] = {
// Actually cross-origin origins
{SecurityOrigin::AccessResultDomainDetail::kDomainNotSet,
"https://example.com", nullptr, "https://not-example.com", nullptr},
{SecurityOrigin::AccessResultDomainDetail::kDomainNotRelevant,
"https://example.com", "example.com", "https://not-example.com",
nullptr},
{SecurityOrigin::AccessResultDomainDetail::kDomainNotRelevant,
"https://example.com", nullptr, "https://not-example.com",
"not-example.com"},
{SecurityOrigin::AccessResultDomainDetail::kDomainNotRelevant,
"https://example.com", "example.com", "https://not-example.com",
"not-example.com"},
// Same-origin origins
{SecurityOrigin::AccessResultDomainDetail::kDomainNotSet,
"https://example.com", nullptr, "https://example.com", nullptr},
{SecurityOrigin::AccessResultDomainDetail::kDomainSetByOnlyOneOrigin,
"https://example.com", "example.com", "https://example.com", nullptr},
{SecurityOrigin::AccessResultDomainDetail::kDomainSetByOnlyOneOrigin,
"https://example.com", nullptr, "https://example.com", "example.com"},
{SecurityOrigin::AccessResultDomainDetail::kDomainMismatch,
"https://www.example.com", "www.example.com", "https://www.example.com",
"example.com"},
{SecurityOrigin::AccessResultDomainDetail::kDomainMatchUnnecessary,
"https://example.com", "example.com", "https://example.com",
"example.com"},
// Same-origin-domain origins
{SecurityOrigin::AccessResultDomainDetail::kDomainNotSet,
"https://a.example.com", nullptr, "https://b.example.com", nullptr},
{SecurityOrigin::AccessResultDomainDetail::kDomainNotRelevant,
"https://a.example.com", "example.com", "https://b.example.com",
nullptr},
{SecurityOrigin::AccessResultDomainDetail::kDomainNotRelevant,
"https://a.example.com", nullptr, "https://b.example.com",
"example.com"},
{SecurityOrigin::AccessResultDomainDetail::kDomainMatchNecessary,
"https://a.example.com", "example.com", "https://b.example.com",
"example.com"},
};
for (TestCase test : tests) {
SCOPED_TRACE(testing::Message()
<< "\nOrigin 1: `" << test.origin1 << "` ("
<< (test.domain1 ? test.domain1 : "") << ") \n"
<< "Origin 2: `" << test.origin2 << "` ("
<< (test.domain2 ? test.domain2 : "") << ")\n");
scoped_refptr<SecurityOrigin> origin1 =
SecurityOrigin::CreateFromString(test.origin1);
if (test.domain1)
origin1->SetDomainFromDOM(test.domain1);
scoped_refptr<SecurityOrigin> origin2 =
SecurityOrigin::CreateFromString(test.origin2);
if (test.domain2)
origin2->SetDomainFromDOM(test.domain2);
SecurityOrigin::AccessResultDomainDetail detail;
origin1->CanAccess(origin2.get(), detail);
EXPECT_EQ(test.expected, detail);
origin2->CanAccess(origin1.get(), detail);
EXPECT_EQ(test.expected, detail);
}
}
TEST_F(SecurityOriginTest, CanRequest) {
struct TestCase {
bool can_request;
......
......@@ -19945,6 +19945,8 @@ Called by update_net_error_codes.py.-->
<int value="2540" label="TextDecoderStreamConstructor"/>
<int value="2541" label="SignedExchangeInnerResponse"/>
<int value="2542" label="PaymentAddressLanguageCode"/>
<int value="2543" label="DocumentDomainBlockedCrossOriginAccess"/>
<int value="2544" label="DocumentDomainEnabledCrossOriginAccess"/>
</enum>
<enum name="FeaturePolicyFeature">
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment