Commit d8d47153 authored by Mike West's avatar Mike West Committed by Commit Bot

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

This is a re-land of [1] after MSAN failures caused a rollback in [2].

[1]: https://chromium-review.googlesource.com/c/chromium/src/+/966442
[2]: https://chromium-review.googlesource.com/c/chromium/src/+/1213152

Bug: 787905
Change-Id: Ie4632a3a482c77202e7e4d3b5c360f021e628389
Reviewed-on: https://chromium-review.googlesource.com/1216684Reviewed-by: default avatarYuki Shiino <yukishiino@chromium.org>
Reviewed-by: default avatarAndy Paicu <andypaicu@chromium.org>
Commit-Queue: Mike West <mkwst@chromium.org>
Cr-Commit-Position: refs/heads/master@{#590219}
parent 3ce3af5c
...@@ -1995,6 +1995,8 @@ enum WebFeature { ...@@ -1995,6 +1995,8 @@ enum WebFeature {
kTextDecoderStreamConstructor = 2540, kTextDecoderStreamConstructor = 2540,
kSignedExchangeInnerResponse = 2541, kSignedExchangeInnerResponse = 2541,
kPaymentAddressLanguageCode = 2542, kPaymentAddressLanguageCode = 2542,
kDocumentDomainBlockedCrossOriginAccess = 2543,
kDocumentDomainEnabledCrossOriginAccess = 2544,
// Add new features immediately above this line. Don't change assigned // Add new features immediately above this line. Don't change assigned
// numbers of any item, and don't reuse removed slots. // numbers of any item, and don't reuse removed slots.
......
...@@ -65,10 +65,22 @@ bool CanAccessWindowInternal(const LocalDOMWindow* accessing_window, ...@@ -65,10 +65,22 @@ bool CanAccessWindowInternal(const LocalDOMWindow* accessing_window,
const SecurityOrigin* accessing_origin = const SecurityOrigin* accessing_origin =
accessing_window->document()->GetSecurityOrigin(); accessing_window->document()->GetSecurityOrigin();
const LocalDOMWindow* local_target_window = ToLocalDOMWindow(target_window); const LocalDOMWindow* local_target_window = ToLocalDOMWindow(target_window);
if (!accessing_origin->CanAccess(
local_target_window->document()->GetSecurityOrigin())) { SecurityOrigin::AccessResultDomainDetail detail;
return false; 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. // Notify the loader's client if the initial document has been accessed.
LocalFrame* target_frame = local_target_window->GetFrame(); LocalFrame* target_frame = local_target_window->GetFrame();
......
...@@ -17,56 +17,97 @@ namespace blink { ...@@ -17,56 +17,97 @@ namespace blink {
namespace { namespace {
const char kMainFrame[] = "https://example.com/main.html"; const char kMainFrame[] = "https://example.com/main.html";
const char kSameOriginTarget[] = "https://example.com/target.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 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 class BindingSecurityCounterTest
: public SimTest, : public SimTest,
public testing::WithParamInterface<const char*> { public testing::WithParamInterface<const char*> {
public: public:
enum class OriginDisposition { CrossOrigin, SameOrigin }; enum class OriginDisposition { CrossOrigin, SameOrigin, SameOriginDomain };
BindingSecurityCounterTest() = default; BindingSecurityCounterTest() = default;
void LoadWindowAndAccessProperty(OriginDisposition which_origin, void LoadWindowAndAccessProperty(OriginDisposition which_origin,
const String& property) { 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 main(kMainFrame, "text/html");
SimRequest target(which_origin == OriginDisposition::CrossOrigin SimRequest target(target_url, "text/html");
? kCrossOriginTarget
: kSameOriginTarget,
"text/html");
const String& document = String::Format( const String& document = String::Format(
"<!DOCTYPE html>" "<!DOCTYPE html>"
"<script>" "<script>"
" %s"
" window.addEventListener('message', e => {" " window.addEventListener('message', e => {"
" window.other = e.source.%s;" " window.other = e.source.%s;"
" console.log('yay');" " console.log('yay');"
" });" " });"
" var w = window.open('%s');" " var w = window.open('%s');"
"</script>", "</script>",
property.Utf8().data(), which_origin == OriginDisposition::SameOriginDomain
which_origin == OriginDisposition::CrossOrigin ? kCrossOriginTarget ? "document.domain = 'example.com';"
: kSameOriginTarget); : "",
property.Utf8().data(), target_url);
LoadURL(kMainFrame); LoadURL(kMainFrame);
main.Complete(document); main.Complete(document);
target.Complete( target.Complete(target_html);
"<!DOCTYPE html>"
"<script>window.opener.postMessage('yay', '*');</script>");
test::RunPendingTasks(); test::RunPendingTasks();
} }
void LoadFrameAndAccessProperty(OriginDisposition which_origin, void LoadFrameAndAccessProperty(OriginDisposition which_origin,
const String& property) { 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 main(kMainFrame, "text/html");
SimRequest target(which_origin == OriginDisposition::CrossOrigin SimRequest target(target_url, "text/html");
? kCrossOriginTarget
: kSameOriginTarget,
"text/html");
const String& document = String::Format( const String& document = String::Format(
"<!DOCTYPE html>" "<!DOCTYPE html>"
"<body>" "<body>"
"<script>" "<script>"
" %s"
" var i = document.createElement('iframe');" " var i = document.createElement('iframe');"
" window.addEventListener('message', e => {" " window.addEventListener('message', e => {"
" window.other = e.source.%s;" " window.other = e.source.%s;"
...@@ -75,15 +116,14 @@ class BindingSecurityCounterTest ...@@ -75,15 +116,14 @@ class BindingSecurityCounterTest
" i.src = '%s';" " i.src = '%s';"
" document.body.appendChild(i);" " document.body.appendChild(i);"
"</script>", "</script>",
property.Utf8().data(), which_origin == OriginDisposition::SameOriginDomain
which_origin == OriginDisposition::CrossOrigin ? kCrossOriginTarget ? "document.domain = 'example.com';"
: kSameOriginTarget); : "",
property.Utf8().data(), target_url);
LoadURL(kMainFrame); LoadURL(kMainFrame);
main.Complete(document); main.Complete(document);
target.Complete( target.Complete(target_html);
"<!DOCTYPE html>"
"<script>window.top.postMessage('yay', '*');</script>");
test::RunPendingTasks(); test::RunPendingTasks();
} }
}; };
...@@ -110,6 +150,8 @@ TEST_P(BindingSecurityCounterTest, CrossOriginWindow) { ...@@ -110,6 +150,8 @@ TEST_P(BindingSecurityCounterTest, CrossOriginWindow) {
WebFeature::kCrossOriginPropertyAccess)); WebFeature::kCrossOriginPropertyAccess));
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement( EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccessFromOpener)); WebFeature::kCrossOriginPropertyAccessFromOpener));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kDocumentDomainEnabledCrossOriginAccess));
} }
TEST_P(BindingSecurityCounterTest, SameOriginWindow) { TEST_P(BindingSecurityCounterTest, SameOriginWindow) {
...@@ -118,6 +160,18 @@ TEST_P(BindingSecurityCounterTest, SameOriginWindow) { ...@@ -118,6 +160,18 @@ TEST_P(BindingSecurityCounterTest, SameOriginWindow) {
WebFeature::kCrossOriginPropertyAccess)); WebFeature::kCrossOriginPropertyAccess));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement( EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccessFromOpener)); 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) { TEST_P(BindingSecurityCounterTest, CrossOriginFrame) {
...@@ -126,6 +180,8 @@ TEST_P(BindingSecurityCounterTest, CrossOriginFrame) { ...@@ -126,6 +180,8 @@ TEST_P(BindingSecurityCounterTest, CrossOriginFrame) {
WebFeature::kCrossOriginPropertyAccess)); WebFeature::kCrossOriginPropertyAccess));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement( EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccessFromOpener)); WebFeature::kCrossOriginPropertyAccessFromOpener));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kDocumentDomainEnabledCrossOriginAccess));
} }
TEST_P(BindingSecurityCounterTest, SameOriginFrame) { TEST_P(BindingSecurityCounterTest, SameOriginFrame) {
...@@ -134,6 +190,18 @@ TEST_P(BindingSecurityCounterTest, SameOriginFrame) { ...@@ -134,6 +190,18 @@ TEST_P(BindingSecurityCounterTest, SameOriginFrame) {
WebFeature::kCrossOriginPropertyAccess)); WebFeature::kCrossOriginPropertyAccess));
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement( EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().HasRecordedMeasurement(
WebFeature::kCrossOriginPropertyAccessFromOpener)); 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 } // namespace
...@@ -254,15 +254,22 @@ bool SecurityOrigin::SerializesAsNull() const { ...@@ -254,15 +254,22 @@ bool SecurityOrigin::SerializesAsNull() const {
return false; return false;
} }
bool SecurityOrigin::CanAccess(const SecurityOrigin* other) const { bool SecurityOrigin::CanAccess(const SecurityOrigin* other,
if (universal_access_) AccessResultDomainDetail& detail) const {
if (universal_access_) {
detail = AccessResultDomainDetail::kDomainNotRelevant;
return true; return true;
}
if (this == other) if (this == other) {
detail = AccessResultDomainDetail::kDomainNotRelevant;
return true; return true;
}
if (IsOpaque() || other->IsOpaque()) if (IsOpaque() || other->IsOpaque()) {
detail = AccessResultDomainDetail::kDomainNotRelevant;
return false; return false;
}
// document.domain handling, as per // document.domain handling, as per
// https://html.spec.whatwg.org/multipage/browsers.html#dom-document-domain: // https://html.spec.whatwg.org/multipage/browsers.html#dom-document-domain:
...@@ -276,6 +283,7 @@ bool SecurityOrigin::CanAccess(const SecurityOrigin* other) const { ...@@ -276,6 +283,7 @@ bool SecurityOrigin::CanAccess(const SecurityOrigin* other) const {
bool can_access = false; bool can_access = false;
if (protocol_ == other->protocol_) { if (protocol_ == other->protocol_) {
if (!domain_was_set_in_dom_ && !other->domain_was_set_in_dom_) { if (!domain_was_set_in_dom_ && !other->domain_was_set_in_dom_) {
detail = AccessResultDomainDetail::kDomainNotSet;
if (host_ == other->host_ && port_ == other->port_) if (host_ == other->host_ && port_ == other->port_)
can_access = true; can_access = true;
} else if (domain_was_set_in_dom_ && other->domain_was_set_in_dom_) { } else if (domain_was_set_in_dom_ && other->domain_was_set_in_dom_) {
...@@ -284,12 +292,27 @@ bool SecurityOrigin::CanAccess(const SecurityOrigin* other) const { ...@@ -284,12 +292,27 @@ bool SecurityOrigin::CanAccess(const SecurityOrigin* other) const {
// https://crbug.com/733150 // https://crbug.com/733150
if (domain_ == other->domain_ && domain_ != "null") { if (domain_ == other->domain_ && domain_ != "null") {
can_access = true; 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;
} }
} else {
detail = AccessResultDomainDetail::kDomainNotRelevant;
} }
if (can_access && IsLocal()) if (can_access && IsLocal() && !PassesFileCheck(other)) {
can_access = PassesFileCheck(other); detail = AccessResultDomainDetail::kDomainNotRelevant;
can_access = false;
}
return can_access; return can_access;
} }
......
...@@ -55,6 +55,15 @@ class PLATFORM_EXPORT SecurityOrigin : public RefCounted<SecurityOrigin> { ...@@ -55,6 +55,15 @@ class PLATFORM_EXPORT SecurityOrigin : public RefCounted<SecurityOrigin> {
WTF_MAKE_NONCOPYABLE(SecurityOrigin); WTF_MAKE_NONCOPYABLE(SecurityOrigin);
public: public:
enum class AccessResultDomainDetail {
kDomainNotRelevant,
kDomainNotSet,
kDomainSetByOnlyOneOrigin,
kDomainMatchNecessary,
kDomainMatchUnnecessary,
kDomainMismatch,
};
static scoped_refptr<SecurityOrigin> Create(const KURL&); static scoped_refptr<SecurityOrigin> Create(const KURL&);
// Creates a new opaque SecurityOrigin that is guaranteed to be cross-origin // Creates a new opaque SecurityOrigin that is guaranteed to be cross-origin
// to all currently existing SecurityOrigins. // to all currently existing SecurityOrigins.
...@@ -112,7 +121,17 @@ class PLATFORM_EXPORT SecurityOrigin : public RefCounted<SecurityOrigin> { ...@@ -112,7 +121,17 @@ class PLATFORM_EXPORT SecurityOrigin : public RefCounted<SecurityOrigin> {
// SecurityOrigin. For example, call this function before allowing // SecurityOrigin. For example, call this function before allowing
// script from one security origin to read or write objects from // script from one security origin to read or write objects from
// another SecurityOrigin. // 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 // Returns true if this SecurityOrigin can read content retrieved from
// the given URL. // the given URL.
......
...@@ -248,6 +248,79 @@ TEST_F(SecurityOriginTest, CanAccess) { ...@@ -248,6 +248,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) { TEST_F(SecurityOriginTest, CanRequest) {
struct TestCase { struct TestCase {
bool can_request; bool can_request;
......
...@@ -19956,6 +19956,8 @@ Called by update_net_error_codes.py.--> ...@@ -19956,6 +19956,8 @@ Called by update_net_error_codes.py.-->
<int value="2540" label="TextDecoderStreamConstructor"/> <int value="2540" label="TextDecoderStreamConstructor"/>
<int value="2541" label="SignedExchangeInnerResponse"/> <int value="2541" label="SignedExchangeInnerResponse"/>
<int value="2542" label="PaymentAddressLanguageCode"/> <int value="2542" label="PaymentAddressLanguageCode"/>
<int value="2543" label="DocumentDomainBlockedCrossOriginAccess"/>
<int value="2544" label="DocumentDomainEnabledCrossOriginAccess"/>
</enum> </enum>
<enum name="FeaturePolicyFeature"> <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