diff options
author | Liam Brady <lbrady@google.com> | 2022-10-21 23:50:32 +0200 |
---|---|---|
committer | Michael BrĂ¼ning <michael.bruning@qt.io> | 2022-12-06 15:46:09 +0000 |
commit | 8dc71a1364d998f9d93ff995e2dd32a0ba4ed371 (patch) | |
tree | a521bc31ce27589abe3b1f85420a094d7de979dd | |
parent | e3bbb5e8fa52c910b959bc8619ddddabc3cc0603 (diff) | |
download | qtwebengine-chromium-8dc71a1364d998f9d93ff995e2dd32a0ba4ed371.tar.gz |
[Backport] CVE-2022-4182: Inappropriate implementation in Fenced Frames
Manual backport of patch originally reviewed on
https://chromium-review.googlesource.com/c/chromium/src/+/3971895:
Fenced frame: have anchor focus require user activation
Currently, focus can be pulled across a fenced frame boundary without
any user activation by using anchor fragments (aka setting location.href
to a.com#anchor). We already have script-based focus properly gated, but
this is a corner case that we missed.
This CL adds a new variable to FocusParams(): `gate_on_user_activation`.
If set to true, then focus that crosses a fenced frame boundary will
only be allowed to happen if the target frame has transient user
activation. This check takes place in
`Frame::AllowFocusWithoutUserActivation()`.
This CL also updates the Focus() call in
`ElementFragmentAnchor::ApplyFocusIfNeeded` to set
`gate_on_user_activation` to true. This has the effect of treating
anchor focusing as a programmatic focus. However, there isn't a
legitimate use case where a fenced frame will need to pull focus into
itself without user gesture using anchor focusing, and, the behavior
will remain unchanged for anchor focusing that does not cross a fenced
frame boundary. So, it's okay to add this restriction.
(cherry picked from commit 855a43d7acc395d80c3932d382061ade2c82626e)
Bug: 1368739
Change-Id: Ia25e96e23e19d780ac8a4c8edb60c0b2472a9e18
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3933078
Reviewed-by: Dave Tapuska <dtapuska@chromium.org>
Commit-Queue: Liam Brady <lbrady@google.com>
Cr-Original-Commit-Position: refs/heads/main@{#1061827}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3971895
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Reviewed-by: Dominic Farolino <dom@chromium.org>
Cr-Commit-Position: refs/branch-heads/5359@{#196}
Cr-Branched-From: 27d3765d341b09369006d030f83f582a29eb57ae-refs/heads/main@{#1058933}
Reviewed-on: https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/446488
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
8 files changed, 70 insertions, 57 deletions
diff --git a/chromium/third_party/blink/renderer/core/dom/element.cc b/chromium/third_party/blink/renderer/core/dom/element.cc index bfbf6b3c103..44d3a815d26 100644 --- a/chromium/third_party/blink/renderer/core/dom/element.cc +++ b/chromium/third_party/blink/renderer/core/dom/element.cc @@ -4905,14 +4905,11 @@ Element* Element::GetAutofocusDelegate() const { return nullptr; } -void Element::focusForBindings() { - focus(FocusParams(SelectionBehaviorOnFocus::kRestore, - mojom::blink::FocusType::kScript, nullptr)); -} - void Element::focusForBindings(const FocusOptions* options) { focus(FocusParams(SelectionBehaviorOnFocus::kRestore, - mojom::blink::FocusType::kScript, nullptr, options)); + mojom::blink::FocusType::kScript, + /*capabilities=*/nullptr, options, + /*gate_on_user_activation=*/true)); } void Element::focus() { @@ -4921,7 +4918,8 @@ void Element::focus() { void Element::focus(const FocusOptions* options) { focus(FocusParams(SelectionBehaviorOnFocus::kRestore, - mojom::blink::FocusType::kNone, nullptr, options)); + mojom::blink::FocusType::kNone, /*capabilities=*/nullptr, + options)); } void Element::focus(const FocusParams& params) { @@ -4952,6 +4950,37 @@ void Element::focus(const FocusParams& params) { return; } + FocusOptions* focus_options = nullptr; + if (params.gate_on_user_activation) { + LocalFrame& frame = *GetDocument().GetFrame(); + if (!frame.AllowFocusWithoutUserActivation() && + !LocalFrame::HasTransientUserActivation(&frame)) { + return; + } + + // Fenced frame focusing should not auto-scroll, since that behavior can + // be observed by an embedder. + if (frame.IsInFencedFrameTree()) { + focus_options = FocusOptions::Create(); + focus_options->setPreventScroll(true); + } + + // Fenced frames should consume user activation when attempting to pull + // focus across a fenced boundary into itself. + // TODO(crbug.com/848778) Right now the browser can't verify that the + // renderer properly consumed user activation. When user activation code is + // migrated to the browser, move this logic to the browser as well. + if (!frame.AllowFocusWithoutUserActivation() && + frame.IsInFencedFrameTree()) { + LocalFrame::ConsumeTransientUserActivation(&frame); + } + } + + FocusParams params_to_use = FocusParams( + params.selection_behavior, params.type, params.source_capabilities, + focus_options ? focus_options : params.options, + params.gate_on_user_activation); + // Ensure we have clean style (including forced display locks). GetDocument().UpdateStyleAndLayoutTreeForNode(this); @@ -4962,9 +4991,9 @@ void Element::focus(const FocusParams& params) { if (Element* new_focus_target = GetFocusableArea()) { // Unlike the specification, we re-run focus() for new_focus_target // because we can't change |this| in a member function. - new_focus_target->focus(FocusParams(SelectionBehaviorOnFocus::kReset, - mojom::blink::FocusType::kForward, - nullptr, params.options)); + new_focus_target->focus(FocusParams( + SelectionBehaviorOnFocus::kReset, mojom::blink::FocusType::kForward, + /*capabilities=*/nullptr, params_to_use.options)); } // 2. If new focus target is null, then: // 2.1. If no fallback target was specified, then return. @@ -4973,12 +5002,13 @@ void Element::focus(const FocusParams& params) { // If a script called focus(), then the type would be kScript. This means // we are activating because of a script action (kScriptFocus). Otherwise, // this is a user activation (kUserFocus). - ActivateDisplayLockIfNeeded(params.type == mojom::blink::FocusType::kScript + ActivateDisplayLockIfNeeded(params_to_use.type == + mojom::blink::FocusType::kScript ? DisplayLockActivationReason::kScriptFocus : DisplayLockActivationReason::kUserFocus); if (!GetDocument().GetPage()->GetFocusController().SetFocusedElement( - this, GetDocument().GetFrame(), params)) + this, GetDocument().GetFrame(), params_to_use)) return; if (GetDocument().FocusedElement() == this) { @@ -4999,14 +5029,15 @@ void Element::focus(const FocusParams& params) { // Trigger a tooltip to show for the newly focused element only when the // focus was set resulting from a keyboard action. // - // TODO(bebeaudr): To also trigger a tooltip when the |params.type| is - // kSpatialNavigation, we'll first have to ensure that the fake mouse move - // event fired by `SpatialNavigationController::DispatchMouseMoveEvent` does - // not lead to a cursor triggered tooltip update. The only tooltip update - // that there should be in that case is the one triggered from the spatial - // navigation keypress. This issue is tracked in https://crbug.com/1206446. + // TODO(bebeaudr): To also trigger a tooltip when the |params_to_use.type| + // is kSpatialNavigation, we'll first have to ensure that the fake mouse + // move event fired by `SpatialNavigationController::DispatchMouseMoveEvent` + // does not lead to a cursor triggered tooltip update. The only tooltip + // update that there should be in that case is the one triggered from the + // spatial navigation keypress. This issue is tracked in + // https://crbug.com/1206446. bool is_focused_from_keypress = false; - switch (params.type) { + switch (params_to_use.type) { case mojom::blink::FocusType::kScript: if (GetDocument() .GetFrame() diff --git a/chromium/third_party/blink/renderer/core/dom/element.h b/chromium/third_party/blink/renderer/core/dom/element.h index 045e5623a9a..50fe406b39f 100644 --- a/chromium/third_party/blink/renderer/core/dom/element.h +++ b/chromium/third_party/blink/renderer/core/dom/element.h @@ -722,7 +722,6 @@ class CORE_EXPORT Element : public ContainerNode, public Animatable { Element* GetFocusableArea() const; Element* GetAutofocusDelegate() const; // Element focus function called through IDL (i.e. element.focus() in JS) - void focusForBindings(); void focusForBindings(const FocusOptions*); // Element focus function called from outside IDL (user focus, // accessibility, etc...) diff --git a/chromium/third_party/blink/renderer/core/dom/focus_params.h b/chromium/third_party/blink/renderer/core/dom/focus_params.h index c6245bcd9ea..670b422e32d 100644 --- a/chromium/third_party/blink/renderer/core/dom/focus_params.h +++ b/chromium/third_party/blink/renderer/core/dom/focus_params.h @@ -17,14 +17,19 @@ struct FocusParams { public: FocusParams() : options(FocusOptions::Create()) {} + explicit FocusParams(bool gate_on_user_activation) + : options(FocusOptions::Create()), + gate_on_user_activation(gate_on_user_activation) {} FocusParams(SelectionBehaviorOnFocus selection, mojom::blink::FocusType focus_type, InputDeviceCapabilities* capabilities, - const FocusOptions* focus_options = FocusOptions::Create()) + const FocusOptions* focus_options = FocusOptions::Create(), + bool gate_on_user_activation = false) : selection_behavior(selection), type(focus_type), source_capabilities(capabilities), - options(focus_options) {} + options(focus_options), + gate_on_user_activation(gate_on_user_activation) {} SelectionBehaviorOnFocus selection_behavior = SelectionBehaviorOnFocus::kRestore; @@ -32,6 +37,7 @@ struct FocusParams { InputDeviceCapabilities* source_capabilities = nullptr; const FocusOptions* options = nullptr; bool omit_blur_events = false; + bool gate_on_user_activation = false; }; } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/frame/dom_window.cc b/chromium/third_party/blink/renderer/core/frame/dom_window.cc index 3f64d9aa75b..8d28001c82b 100644 --- a/chromium/third_party/blink/renderer/core/frame/dom_window.cc +++ b/chromium/third_party/blink/renderer/core/frame/dom_window.cc @@ -440,7 +440,7 @@ void DOMWindow::focus(v8::Isolate* isolate) { if (!page) return; - if (!frame->ShouldAllowScriptFocus()) { + if (!frame->AllowFocusWithoutUserActivation()) { // Disallow script focus that crosses a fenced frame boundary on a // frame that doesn't have transient user activation. Note: all calls to // DOMWindow::focus come from JavaScript calls in the web platform diff --git a/chromium/third_party/blink/renderer/core/frame/frame.cc b/chromium/third_party/blink/renderer/core/frame/frame.cc index a519c49a019..8821bea24bd 100644 --- a/chromium/third_party/blink/renderer/core/frame/frame.cc +++ b/chromium/third_party/blink/renderer/core/frame/frame.cc @@ -698,7 +698,7 @@ bool Frame::FocusCrossesFencedBoundary() { return false; } -bool Frame::ShouldAllowScriptFocus() { +bool Frame::AllowFocusWithoutUserActivation() { if (!features::IsFencedFramesEnabled()) return true; diff --git a/chromium/third_party/blink/renderer/core/frame/frame.h b/chromium/third_party/blink/renderer/core/frame/frame.h index 9ca0647670f..c1a5f9bc17a 100644 --- a/chromium/third_party/blink/renderer/core/frame/frame.h +++ b/chromium/third_party/blink/renderer/core/frame/frame.h @@ -213,9 +213,13 @@ class CORE_EXPORT Frame : public GarbageCollected<Frame> { void SetIsLoading(bool is_loading) { is_loading_ = is_loading; } bool IsLoading() const { return is_loading_; } - // Determines if the frame should be allowed to pull focus from a JavaScript - // call. - bool ShouldAllowScriptFocus(); + // Determines if the frame should be allowed to pull focus without receiving + // user activation. A frame cannot pull focus without user activation if + // doing so would cross a fenced frame boundary. + // Note: the only time focus can be pulled across a fence without the target + // frame having user activation is in the case of tab-focusing. In that case, + // this function is not called and focus is blanket allowed. + bool AllowFocusWithoutUserActivation(); // Tells the frame to check whether its load has completed, based on the state // of its subframes, etc. diff --git a/chromium/third_party/blink/renderer/core/page/focus_controller.cc b/chromium/third_party/blink/renderer/core/page/focus_controller.cc index f627814af46..66c720ea596 100644 --- a/chromium/third_party/blink/renderer/core/page/focus_controller.cc +++ b/chromium/third_party/blink/renderer/core/page/focus_controller.cc @@ -1296,34 +1296,6 @@ bool FocusController::SetFocusedElement(Element* element, new_document->FocusedElement() == element) return true; - // Fenced frame focusing should not auto-scroll, since that behavior can - // be observed by an embedder. - FocusParams params_to_use = params; - if (new_document && params.type == mojom::blink::FocusType::kScript && - new_document->GetFrame()->IsInFencedFrameTree()) { - FocusOptions* focus_options = FocusOptions::Create(); - focus_options->setPreventScroll(true); - params_to_use = FocusParams(params.selection_behavior, params.type, - params.source_capabilities, focus_options); - } - - if (new_focused_frame && !new_focused_frame->ShouldAllowScriptFocus() && - params_to_use.type == mojom::blink::FocusType::kScript) { - // Disallow script focus that crosses a fenced frame boundary on a - // frame that doesn't have transient user activation. - if (!new_focused_frame->HasTransientUserActivation()) - return false; - // Fenced frames should consume user activation when attempting to pull - // focus across a fenced boundary into itself. - // TODO(crbug.com/1123606) Right now the browser can't verify that the - // renderer properly consumed user activation. When user activation code is - // migrated to the browser, move this logic to the browser as well. - if (new_focused_frame->IsInFencedFrameTree()) { - LocalFrame::ConsumeTransientUserActivation( - DynamicTo<LocalFrame>(new_focused_frame)); - } - } - if (old_document && old_document != new_document) old_document->ClearFocusedElement(); @@ -1336,7 +1308,7 @@ bool FocusController::SetFocusedElement(Element* element, if (new_document) { bool successfully_focused = - new_document->SetFocusedElement(element, params_to_use); + new_document->SetFocusedElement(element, params); if (!successfully_focused) return false; diff --git a/chromium/third_party/blink/renderer/core/page/scrolling/element_fragment_anchor.cc b/chromium/third_party/blink/renderer/core/page/scrolling/element_fragment_anchor.cc index a5bacdbe09e..443630ed601 100644 --- a/chromium/third_party/blink/renderer/core/page/scrolling/element_fragment_anchor.cc +++ b/chromium/third_party/blink/renderer/core/page/scrolling/element_fragment_anchor.cc @@ -10,6 +10,7 @@ #include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h" #include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/dom/element.h" +#include "third_party/blink/renderer/core/dom/focus_params.h" #include "third_party/blink/renderer/core/dom/node.h" #include "third_party/blink/renderer/core/editing/frame_selection.h" #include "third_party/blink/renderer/core/execution_context/execution_context.h" @@ -203,7 +204,7 @@ void ElementFragmentAnchor::ApplyFocusIfNeeded() { // clear focus, which matches the behavior of other browsers. auto* element = DynamicTo<Element>(anchor_node_.Get()); if (element && element->IsFocusable()) { - element->focus(); + element->focus(FocusParams(/*gate_on_user_activation=*/true)); } else { frame_->GetDocument()->SetSequentialFocusNavigationStartingPoint( anchor_node_); |