| /* |
| * 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 |