Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace in Frames
DescriptionRace in Frames
ComponentFrames
Bug ClassRace
Tracker371247941
Fix commit7b2e3f7ff30d (chromium/src) +105/-19
CISA KEVNot listed
CreditedHafiizh
Disclosed2025-01-14

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/frame/frame_view.cc
modified
async_test
third_party/blink/web_tests/external/wpt/intersection-observer/v2/nested-cross-origin.sub.html
modified

Files Changed

  • third_party/blink/renderer/core/frame/frame_view.cc
  • third_party/blink/renderer/core/intersection_observer/intersection_observation.cc
  • third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-midframe.sub.html
  • third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-subframe.html
  • third_party/blink/web_tests/external/wpt/intersection-observer/v2/nested-cross-origin.sub.html
From 7b2e3f7ff30d5dcf17cd5c00f0554a44eec2c2a1 Mon Sep 17 00:00:00 2001
From: Stefan Zager <szager@chromium.org>
Date: Thu, 24 Oct 2024 02:35:39 +0000
Subject: [PATCH] IntersectionObserver -- properly handle "unknown" occlusion state

If we most recently reported a target as "guaranteed visible", then in
the interest of avoiding false positives we must transition to "not
guaranteed visible" if the frame occlusion state becomes "unknown".

This CL also makes a child frame inherit its parent's "not visible"
occlusion state rather than calling it "unknown", which is technically
more correct.

Bug: chromium:371247941
Change-Id: I4d721dd252d013deac14a12f1f2922830ef2a8a4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5950965
Reviewed-by: Xianzhu Wang <wangxianzhu@chromium.org>
Commit-Queue: Stefan Zager <szager@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1373093}
---

diff --git a/third_party/blink/renderer/core/frame/frame_view.cc b/third_party/blink/renderer/core/frame/frame_view.cc
index 3d6d9a2..29981ce52 100644
--- a/third_party/blink/renderer/core/frame/frame_view.cc
+++ b/third_party/blink/renderer/core/frame/frame_view.cc
@@ -77,15 +77,20 @@
   gfx::Transform main_frame_transform_matrix;
   DocumentLifecycle::LifecycleState parent_lifecycle_state =
       owner_document.Lifecycle().GetState();
+
+  bool should_compute_occlusion = false;
   mojom::blink::FrameOcclusionState occlusion_state =
       owner_document.GetFrame()->GetOcclusionState();
-  bool should_compute_occlusion =
-      needs_occlusion_tracking &&
-      occlusion_state ==
-          mojom::blink::FrameOcclusionState::kGuaranteedNotOccluded &&
-      parent_lifecycle_state >= DocumentLifecycle::kPrePaintClean;
-  if (!should_compute_occlusion) {
+  if (occlusion_state ==
+      mojom::blink::FrameOcclusionState::kGuaranteedNotOccluded) {
+    // We can't propagate kGuaranteedNotOccluded from the parent without testing
+    // occlusion of this frame. If we don't ultimately do an occlusion test on
+    // this frame, then we should propagate "unknown".
     occlusion_state = mojom::blink::FrameOcclusionState::kUnknown;
+    if (needs_occlusion_tracking &&
+        parent_lifecycle_state >= DocumentLifecycle::kPrePaintClean) {
+      should_compute_occlusion = true;
+    }
   }
 
   LayoutEmbeddedContent* owner_layout_object =
@@ -159,8 +164,12 @@
         rect_in_parent_stable_since_for_iov2_ = base::TimeTicks::Now();
       }
     }
-    if (should_compute_occlusion && !geometry.IsVisible())
-      occlusion_state = mojom::blink::FrameOcclusionState::kPossiblyOccluded;
+    if (should_compute_occlusion) {
+      occlusion_state =
+          geometry.IsVisible()
+              ? mojom::blink::FrameOcclusionState::kGuaranteedNotOccluded
+              : mojom::blink::FrameOcclusionState::kPossiblyOccluded;
+    }
 
     // Generate matrix to transform from the space of the containing document
     // to the space of the iframe's contents.
@@ -248,11 +257,6 @@
     }
     main_frame_transform_matrix =
         child_frame_to_root_frame.AccumulatedTransform();
-  } else if (occlusion_state ==
-             mojom::blink::FrameOcclusionState::kGuaranteedNotOccluded) {
-    // If the parent LocalFrameView is throttled and out-of-date, then we can't
-    // get any useful information.
-    occlusion_state = mojom::blink::FrameOcclusionState::kUnknown;
   }
 
   // An iframe's content is always pixel-snapped, even if the iframe element has
diff --git a/third_party/blink/renderer/core/intersection_observer/intersection_observation.cc b/third_party/blink/renderer/core/intersection_observer/intersection_observation.cc
index a05620d..160254b6 100644
--- a/third_party/blink/renderer/core/intersection_observer/intersection_observation.cc
+++ b/third_party/blink/renderer/core/intersection_observer/intersection_observation.cc
@@ -179,11 +179,14 @@
       Observer()->trackVisibility()) {
     mojom::blink::FrameOcclusionState occlusion_state =
         target_->GetDocument().GetFrame()->GetOcclusionState();
-    // If we're tracking visibility, and we don't have occlusion information
-    // from our parent frame, then postpone computing intersections until a
-    // later lifecycle when the occlusion information is known.
-    if (occlusion_state == mojom::blink::FrameOcclusionState::kUnknown)
+    // If we're tracking visibility, and we aren't currently reporting the
+    // target visible, and we don't have occlusion information from our parent
+    // frame, then postpone computing intersections until a later lifecycle when
+    // the occlusion information is known.
+    if (!last_is_visible_ &&
+        occlusion_state == mojom::blink::FrameOcclusionState::kUnknown) {
       return false;
+    }
   }
   return true;
 }
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-midframe.sub.html b/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-midframe.sub.html
new file mode 100644
index 0000000..b9b055b6
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-midframe.sub.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<style>
+iframe {
+  width: 300px;
+  height: 150px;
+  border: none;
+}
+</style>
+
+<iframe src="http://{{domains[www2]}}:{{ports[http][0]}}/intersection-observer/resources/v2-subframe.html"></iframe>
+
+<script>
+window.addEventListener("message", event => {
+  requestAnimationFrame(() => setTimeout(() => {
+    document.querySelector('iframe').contentWindow.postMessage(event.data, "*");
+  }));
+});
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-subframe.html b/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-subframe.html
index 295bbf04..cfc82646 100644
--- a/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-subframe.html
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-subframe.html
@@ -16,7 +16,7 @@
 
 window.addEventListener("message", event => {
   waitForNotification(() => {
-    window.parent.postMessage(results.map(e => e.isVisible), "*");
+    window.top.postMessage(results.map(e => e.isVisible), "*");
     results = [];
   });
 });
@@ -27,6 +27,6 @@
     results = entries;
   }, {trackVisibility: true, delay: delay});
   observer.observe(document.getElementById("target"));
-  window.parent.postMessage("", "*");
+  window.top.postMessage("", "*");
 };
 </script>
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/v2/nested-cross-origin.sub.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/nested-cross-origin.sub.html
new file mode 100644
index 0000000..42a69bf
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/nested-cross-origin.sub.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 300px;
+  height: 150px;
+  border: none;
+}
+</style>
+
+<iframe src="http://{{domains[www1]}}:{{ports[http][0]}}/intersection-observer/resources/v2-midframe.sub.html"></iframe>
+
+<script>
+async_test(function(t) {
+  let iframe = document.querySelector("iframe");
+
+  function step0(event) {
+    assert_equals(event.data,"");
+  }
+
+  function step1(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+    iframe.style.opacity = "0.9";
+  }
+
+  function step2(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([false]));
+    iframe.style.opacity = "";
+  }
+
+  function step3(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  let steps = [step0, step1, step2, step3];
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-midframe.sub.html b/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-midframe.sub.html
new file mode 100644
index 0000000..b9b055b6
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-midframe.sub.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<style>
+iframe {
+  width: 300px;
+  height: 150px;
+  border: none;
+}
+</style>
+
+<iframe src="http://{{domains[www2]}}:{{ports[http][0]}}/intersection-observer/resources/v2-subframe.html"></iframe>
+
+<script>
+window.addEventListener("message", event => {
+  requestAnimationFrame(() => setTimeout(() => {
+    document.querySelector('iframe').contentWindow.postMessage(event.data, "*");
+  }));
+});
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-subframe.html b/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-subframe.html
index 295bbf04..cfc82646 100644
--- a/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-subframe.html
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/resources/v2-subframe.html
@@ -16,7 +16,7 @@
 
 window.addEventListener("message", event => {
   waitForNotification(() => {
-    window.parent.postMessage(results.map(e => e.isVisible), "*");
+    window.top.postMessage(results.map(e => e.isVisible), "*");
     results = [];
   });
 });
@@ -27,6 +27,6 @@
     results = entries;
   }, {trackVisibility: true, delay: delay});
   observer.observe(document.getElementById("target"));
-  window.parent.postMessage("", "*");
+  window.top.postMessage("", "*");
 };
 </script>
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/v2/nested-cross-origin.sub.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/nested-cross-origin.sub.html
new file mode 100644
index 0000000..42a69bf
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/nested-cross-origin.sub.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 300px;
+  height: 150px;
+  border: none;
+}
+</style>
+
+<iframe src="http://{{domains[www1]}}:{{ports[http][0]}}/intersection-observer/resources/v2-midframe.sub.html"></iframe>
+
+<script>
+async_test(function(t) {
+  let iframe = document.querySelector("iframe");
+
+  function step0(event) {
+    assert_equals(event.data,"");
+  }
+
+  function step1(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+    iframe.style.opacity = "0.9";
+  }
+
+  function step2(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([false]));
+    iframe.style.opacity = "";
+  }
+
+  function step3(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  let steps = [step0, step1, step2, step3];
+
+  window.addEventListener("message", event => {
+    if (steps.length) {
+      t.step(steps.shift(), t, event);
+      waitForFrame(t, () => {
+        iframe.contentWindow.postMessage("", "*");
+      });
+    } else {
+      t.done();
+    }
+  });
+
+}, "Intersection observer V2 test with nested cross-origin iframes.");
+</script>
Loading diff…

Original Bug Report

reported by sa...@gmail.com

Bypass of https://issues.chromium.org/issues/333708039

VULNERABILITY DETAILS

This vulnerability is similar to https://issues.chromium.org/issues/333708039, in this bug when the cursor focus on google one tap button after that the opacity the frame set 0 (obscured by “click me see funny cats” button) the focus still to google tap button lead to click jacking

VERSION Chrome Version 131.0.6755.0 (Official Build) canary (64-bit) Operating System: Windows 10

REPRODUCTION CASE

  1. open https://thundering-unruly-windflower.glitch.me/spoofh.html
  2. click on “click me see funny cats” button

CREDIT INFORMATION Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: Hafiizh (https://www.linkedin.com/in/hafiizh-7aa6bb31/)

View on issue tracker