blob: 9339a01ec81b681c1f5709cd8b32fd11679f3de0 [file] [log] [blame]
/*
* Copyright (C) 2009 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/bindings/core/v8/binding_security.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_location.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_window.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/execution_context/agent.h"
#include "third_party/blink/renderer/core/frame/dom_window.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/location.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/html/html_frame_element_base.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/web_test_support.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
namespace blink {
namespace {
// Documents that have the same WindowAgentFactory should be able to
// share data with each other if they have the same Agent and are
// SameOriginDomain.
bool IsSameWindowAgentFactory(const LocalDOMWindow* window1,
const LocalDOMWindow* window2) {
return window1->GetFrame() && window2->GetFrame() &&
&window1->GetFrame()->window_agent_factory() ==
&window2->GetFrame()->window_agent_factory();
}
} // namespace
void BindingSecurity::Init() {
BindingSecurityForPlatform::SetShouldAllowAccessToV8ContextWithExceptionState(
ShouldAllowAccessToV8Context);
BindingSecurityForPlatform::
SetShouldAllowAccessToV8ContextWithErrorReportOption(
ShouldAllowAccessToV8Context);
BindingSecurityForPlatform::SetShouldAllowWrapperCreationOrThrowException(
ShouldAllowWrapperCreationOrThrowException);
BindingSecurityForPlatform::SetRethrowWrapperCreationException(
RethrowWrapperCreationException);
}
namespace {
void ReportOrThrowSecurityError(
const LocalDOMWindow* accessing_window,
const DOMWindow* target_window,
DOMWindow::CrossDocumentAccessPolicy cross_document_access,
ExceptionState& exception_state) {
if (target_window) {
exception_state.ThrowSecurityError(
target_window->SanitizedCrossDomainAccessErrorMessage(
accessing_window, cross_document_access),
target_window->CrossDomainAccessErrorMessage(accessing_window,
cross_document_access));
} else {
exception_state.ThrowSecurityError("Cross origin access was denied.");
}
}
void ReportOrThrowSecurityError(
const LocalDOMWindow* accessing_window,
const DOMWindow* target_window,
DOMWindow::CrossDocumentAccessPolicy cross_document_access,
BindingSecurity::ErrorReportOption reporting_option) {
if (reporting_option == BindingSecurity::ErrorReportOption::kDoNotReport)
return;
if (accessing_window && target_window) {
accessing_window->PrintErrorMessage(
target_window->CrossDomainAccessErrorMessage(accessing_window,
cross_document_access));
} else if (accessing_window) {
accessing_window->PrintErrorMessage("Cross origin access was denied.");
} else {
// Nowhere to report the error.
}
}
bool CanAccessWindowInternal(
const LocalDOMWindow* accessing_window,
const DOMWindow* target_window,
DOMWindow::CrossDocumentAccessPolicy* cross_document_access) {
SECURITY_CHECK(!(target_window && target_window->GetFrame()) ||
target_window == target_window->GetFrame()->DomWindow());
DCHECK_EQ(DOMWindow::CrossDocumentAccessPolicy::kAllowed,
*cross_document_access);
// It's important to check that target_window is a LocalDOMWindow: it's
// possible for a remote frame and local frame to have the same security
// origin, depending on the model being used to allocate Frames between
// processes. See https://crbug.com/601629.
const auto* local_target_window = DynamicTo<LocalDOMWindow>(target_window);
if (!(accessing_window && local_target_window))
return false;
const SecurityOrigin* accessing_origin =
accessing_window->GetSecurityOrigin();
SecurityOrigin::AccessResultDomainDetail detail;
bool can_access = accessing_origin->CanAccess(
local_target_window->GetSecurityOrigin(), detail);
if (detail ==
SecurityOrigin::AccessResultDomainDetail::kDomainSetByOnlyOneOrigin ||
detail ==
SecurityOrigin::AccessResultDomainDetail::kDomainMatchNecessary ||
detail == SecurityOrigin::AccessResultDomainDetail::kDomainMismatch) {
UseCounter::Count(
accessing_window->document(),
can_access ? WebFeature::kDocumentDomainEnabledCrossOriginAccess
: WebFeature::kDocumentDomainBlockedCrossOriginAccess);
}
if (!can_access) {
// Ensure that if we got a cluster mismatch that it was due to a feature
// policy being enabled and not a logic bug.
if (detail == SecurityOrigin::AccessResultDomainDetail::
kDomainNotRelevantAgentClusterMismatch) {
// Assert that because the agent clusters are different than the
// WindowAgentFactories must also be different unless they differ in
// being explicitly origin keyed.
SECURITY_CHECK(
!IsSameWindowAgentFactory(accessing_window, local_target_window) ||
(accessing_window->GetAgent()->IsExplicitlyOriginKeyed() !=
local_target_window->GetAgent()->IsExplicitlyOriginKeyed()) ||
(WebTestSupport::IsRunningWebTest() &&
local_target_window->GetFrame()->PagePopupOwner()));
*cross_document_access =
DOMWindow::CrossDocumentAccessPolicy::kDisallowed;
}
return false;
}
// Notify the loader's client if the initial document has been accessed.
LocalFrame* target_frame = local_target_window->GetFrame();
if (target_frame && target_frame->GetDocument()->IsInitialEmptyDocument()) {
target_frame->Loader().DidAccessInitialDocument();
}
return true;
}
template <typename ExceptionStateOrErrorReportOption>
bool CanAccessWindow(const LocalDOMWindow* accessing_window,
const DOMWindow* target_window,
ExceptionStateOrErrorReportOption& error_report) {
DOMWindow::CrossDocumentAccessPolicy cross_document_access =
DOMWindow::CrossDocumentAccessPolicy::kAllowed;
if (CanAccessWindowInternal(accessing_window, target_window,
&cross_document_access))
return true;
ReportOrThrowSecurityError(accessing_window, target_window,
cross_document_access, error_report);
return false;
}
DOMWindow* FindWindow(v8::Isolate* isolate,
const WrapperTypeInfo* type,
v8::Local<v8::Object> holder) {
if (V8Window::GetWrapperTypeInfo()->Equals(type))
return V8Window::ToImpl(holder);
if (V8Location::GetWrapperTypeInfo()->Equals(type))
return V8Location::ToImpl(holder)->DomWindow();
// This function can handle only those types listed above.
NOTREACHED();
return nullptr;
}
} // namespace
bool BindingSecurity::ShouldAllowAccessTo(
const LocalDOMWindow* accessing_window,
const DOMWindow* target,
ExceptionState& exception_state) {
DCHECK(target);
// TODO(https://crbug.com/723057): This is intended to match the legacy
// behavior of when access checks revolved around Frame pointers rather than
// DOMWindow pointers. This prevents web-visible behavior changes, since the
// previous implementation had to follow the back pointer to the Frame, and
// would have to early return when it was null.
if (!target->GetFrame())
return false;
bool can_access = CanAccessWindow(accessing_window, target, exception_state);
if (!can_access && accessing_window) {
UseCounter::Count(accessing_window->document(),
WebFeature::kCrossOriginPropertyAccess);
if (target->opener() == accessing_window) {
UseCounter::Count(accessing_window->document(),
WebFeature::kCrossOriginPropertyAccessFromOpener);
}
}
return can_access;
}
bool BindingSecurity::ShouldAllowAccessTo(
const LocalDOMWindow* accessing_window,
const DOMWindow* target,
ErrorReportOption reporting_option) {
DCHECK(target);
// TODO(https://crbug.com/723057): This is intended to match the legacy
// behavior of when access checks revolved around Frame pointers rather than
// DOMWindow pointers. This prevents web-visible behavior changes, since the
// previous implementation had to follow the back pointer to the Frame, and
// would have to early return when it was null.
if (!target->GetFrame())
return false;
bool can_access = CanAccessWindow(accessing_window, target, reporting_option);
if (!can_access && accessing_window) {
UseCounter::Count(accessing_window->document(),
WebFeature::kCrossOriginPropertyAccess);
if (target->opener() == accessing_window) {
UseCounter::Count(accessing_window->document(),
WebFeature::kCrossOriginPropertyAccessFromOpener);
}
}
return can_access;
}
bool BindingSecurity::ShouldAllowAccessTo(
const LocalDOMWindow* accessing_window,
const Location* target,
ExceptionState& exception_state) {
DCHECK(target);
// TODO(https://crbug.com/723057): This is intended to match the legacy
// behavior of when access checks revolved around Frame pointers rather than
// DOMWindow pointers. This prevents web-visible behavior changes, since the
// previous implementation had to follow the back pointer to the Frame, and
// would have to early return when it was null.
if (!target->DomWindow()->GetFrame())
return false;
bool can_access =
CanAccessWindow(accessing_window, target->DomWindow(), exception_state);
if (!can_access && accessing_window) {
UseCounter::Count(accessing_window->document(),
WebFeature::kCrossOriginPropertyAccess);
if (target->DomWindow()->opener() == accessing_window) {
UseCounter::Count(accessing_window->document(),
WebFeature::kCrossOriginPropertyAccessFromOpener);
}
}
return can_access;
}
bool BindingSecurity::ShouldAllowAccessTo(
const LocalDOMWindow* accessing_window,
const Location* target,
ErrorReportOption reporting_option) {
DCHECK(target);
// TODO(https://crbug.com/723057): This is intended to match the legacy
// behavior of when access checks revolved around Frame pointers rather than
// DOMWindow pointers. This prevents web-visible behavior changes, since the
// previous implementation had to follow the back pointer to the Frame, and
// would have to early return when it was null.
if (!target->DomWindow()->GetFrame())
return false;
bool can_access =
CanAccessWindow(accessing_window, target->DomWindow(), reporting_option);
if (!can_access && accessing_window) {
UseCounter::Count(accessing_window->document(),
WebFeature::kCrossOriginPropertyAccess);
if (target->DomWindow()->opener() == accessing_window) {
UseCounter::Count(accessing_window->document(),
WebFeature::kCrossOriginPropertyAccessFromOpener);
}
}
return can_access;
}
bool BindingSecurity::ShouldAllowAccessTo(
const LocalDOMWindow* accessing_window,
const Node* target,
ExceptionState& exception_state) {
if (!target)
return false;
return CanAccessWindow(accessing_window, target->GetDocument().domWindow(),
exception_state);
}
bool BindingSecurity::ShouldAllowAccessTo(
const LocalDOMWindow* accessing_window,
const Node* target,
ErrorReportOption reporting_option) {
if (!target)
return false;
return CanAccessWindow(accessing_window, target->GetDocument().domWindow(),
reporting_option);
}
bool BindingSecurity::ShouldAllowAccessToFrame(
const LocalDOMWindow* accessing_window,
const Frame* target,
ExceptionState& exception_state) {
if (!target || !target->GetSecurityContext())
return false;
return CanAccessWindow(accessing_window, target->DomWindow(),
exception_state);
}
bool BindingSecurity::ShouldAllowAccessToFrame(
const LocalDOMWindow* accessing_window,
const Frame* target,
ErrorReportOption reporting_option) {
if (!target || !target->GetSecurityContext())
return false;
return CanAccessWindow(accessing_window, target->DomWindow(),
reporting_option);
}
namespace {
template <typename ExceptionStateOrErrorReportOption>
bool ShouldAllowAccessToV8ContextInternal(
v8::Local<v8::Context> accessing_context,
v8::Local<v8::Context> target_context,
ExceptionStateOrErrorReportOption& error_report) {
// Fast path for the most likely case.
if (accessing_context == target_context)
return true;
// Workers and worklets do not support multiple contexts, so both of
// |accessing_context| and |target_context| must be windows at this point.
// remote_object->CreationContext() returns the empty handle. Remote contexts
// are unconditionally treated as cross origin.
if (target_context.IsEmpty()) {
ReportOrThrowSecurityError(ToLocalDOMWindow(accessing_context), nullptr,
DOMWindow::CrossDocumentAccessPolicy::kAllowed,
error_report);
return false;
}
LocalFrame* target_frame = ToLocalFrameIfNotDetached(target_context);
// TODO(dcheng): Why doesn't this code just use DOMWindows throughout? Can't
// we just always use ToLocalDOMWindow(context)?
if (!target_frame) {
// Sandbox detached frames - they can't create cross origin objects.
LocalDOMWindow* accessing_window = ToLocalDOMWindow(accessing_context);
LocalDOMWindow* target_window = ToLocalDOMWindow(target_context);
// TODO(https://crbug.com/723057): This is tricky: this intentionally uses
// the internal CanAccessWindow() helper rather than ShouldAllowAccessTo().
// ShouldAllowAccessTo() unconditionally denies access if the DOMWindow is
// not attached to a Frame, but this code is intended for handling the
// detached DOMWindow case.
return CanAccessWindow(accessing_window, target_window, error_report);
}
const DOMWrapperWorld& accessing_world =
DOMWrapperWorld::World(accessing_context);
const DOMWrapperWorld& target_world = DOMWrapperWorld::World(target_context);
CHECK_EQ(accessing_world.GetWorldId(), target_world.GetWorldId());
return !accessing_world.IsMainWorld() ||
BindingSecurity::ShouldAllowAccessToFrame(
ToLocalDOMWindow(accessing_context), target_frame, error_report);
}
} // namespace
bool BindingSecurity::ShouldAllowAccessToV8Context(
v8::Local<v8::Context> accessing_context,
v8::Local<v8::Context> target_context,
ExceptionState& exception_state) {
return ShouldAllowAccessToV8ContextInternal(accessing_context, target_context,
exception_state);
}
bool BindingSecurity::ShouldAllowAccessToV8Context(
v8::Local<v8::Context> accessing_context,
v8::Local<v8::Context> target_context,
ErrorReportOption reporting_option) {
return ShouldAllowAccessToV8ContextInternal(accessing_context, target_context,
reporting_option);
}
bool BindingSecurity::ShouldAllowWrapperCreationOrThrowException(
v8::Local<v8::Context> accessing_context,
v8::Local<v8::Context> creation_context,
const WrapperTypeInfo* wrapper_type_info) {
// Fast path for the most likely case.
if (accessing_context == creation_context)
return true;
// According to
// https://html.spec.whatwg.org/C/#security-location,
// cross-origin script access to a few properties of Location is allowed.
// Location already implements the necessary security checks.
if (wrapper_type_info->Equals(V8Location::GetWrapperTypeInfo()))
return true;
ExceptionState exception_state(accessing_context->GetIsolate(),
ExceptionState::kConstructionContext,
wrapper_type_info->interface_name);
return ShouldAllowAccessToV8Context(accessing_context, creation_context,
exception_state);
}
void BindingSecurity::RethrowWrapperCreationException(
v8::Local<v8::Context> accessing_context,
v8::Local<v8::Context> creation_context,
const WrapperTypeInfo* wrapper_type_info,
v8::Local<v8::Value> cross_context_exception) {
DCHECK(!cross_context_exception.IsEmpty());
v8::Isolate* isolate = creation_context->GetIsolate();
ExceptionState exception_state(isolate, ExceptionState::kConstructionContext,
wrapper_type_info->interface_name);
if (!ShouldAllowAccessToV8Context(accessing_context, creation_context,
exception_state)) {
// A cross origin exception has turned into a SecurityError.
CHECK(exception_state.HadException());
return;
}
exception_state.RethrowV8Exception(cross_context_exception);
}
void BindingSecurity::FailedAccessCheckFor(v8::Isolate* isolate,
const WrapperTypeInfo* type,
v8::Local<v8::Object> holder) {
DOMWindow* target = FindWindow(isolate, type, holder);
// Failing to find a target means something is wrong. Failing to throw an
// exception could be a security issue, so just crash.
CHECK(target);
// TODO(https://crbug.com/723057): This is intended to match the legacy
// behavior of when access checks revolved around Frame pointers rather than
// DOMWindow pointers. This prevents web-visible behavior changes, since the
// previous implementation had to follow the back pointer to the Frame, and
// would have to early return when it was null.
if (!target->GetFrame())
return;
auto* local_dom_window = CurrentDOMWindow(isolate);
// Determine if the access check failure was because of cross-origin or if the
// WindowAgentFactory is different. If the WindowAgentFactories are different
// it indicates that the "disallowdocumentaccess" attribute was used on an
// iframe somewhere in the ancestor chain so report the error as "restricted"
// instead of "cross-origin".
DOMWindow::CrossDocumentAccessPolicy cross_document_access =
(!target->ToLocalDOMWindow() ||
IsSameWindowAgentFactory(local_dom_window, target->ToLocalDOMWindow()))
? DOMWindow::CrossDocumentAccessPolicy::kAllowed
: DOMWindow::CrossDocumentAccessPolicy::kDisallowed;
// TODO(dcheng): Add ContextType, interface name, and property name as
// arguments, so the generated exception can be more descriptive.
ExceptionState exception_state(isolate, ExceptionState::kUnknownContext,
nullptr, nullptr);
exception_state.ThrowSecurityError(
target->SanitizedCrossDomainAccessErrorMessage(local_dom_window,
cross_document_access),
target->CrossDomainAccessErrorMessage(local_dom_window,
cross_document_access));
}
bool BindingSecurity::ShouldAllowNamedAccessTo(
const DOMWindow* accessing_window,
const DOMWindow* target_window) {
const Frame* accessing_frame = accessing_window->GetFrame();
DCHECK(accessing_frame);
DCHECK(accessing_frame->GetSecurityContext());
const SecurityOrigin* accessing_origin =
accessing_frame->GetSecurityContext()->GetSecurityOrigin();
const Frame* target_frame = target_window->GetFrame();
DCHECK(target_frame);
DCHECK(target_frame->GetSecurityContext());
const SecurityOrigin* target_origin =
target_frame->GetSecurityContext()->GetSecurityOrigin();
SECURITY_CHECK(!(target_window && target_window->GetFrame()) ||
target_window == target_window->GetFrame()->DomWindow());
if (!accessing_origin->CanAccess(target_origin))
return false;
// Note that there is no need to call back
// FrameLoader::didAccessInitialDocument() because |targetWindow| must be
// a child window inside iframe or frame and it doesn't have a URL bar,
// so there is no need to worry about URL spoofing.
return true;
}
} // namespace blink