Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in SVG
DescriptionInsufficient policy enforcement in SVG
ComponentSVG
Bug ClassLogic Error
Tracker502505715
Fix commit17a3040239ed (chromium/src) +153/-20
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/html/anchor_element_utils.cc
modified
if
third_party/blink/renderer/core/html/html_anchor_element.cc
modified

Files Changed

  • third_party/blink/renderer/core/html/DEPS
  • third_party/blink/renderer/core/html/anchor_element_utils.cc
  • third_party/blink/renderer/core/html/anchor_element_utils.h
  • third_party/blink/renderer/core/html/html_anchor_element.cc
  • third_party/blink/renderer/core/svg/svg_a_element.cc
  • third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
  • third_party/blink/web_tests/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html
From 17a3040239ed7e02e18ba84157520c9ba052b8ae Mon Sep 17 00:00:00 2001
From: Divyansh Mangal <[email protected]>
Date: Wed, 03 Jun 2026 23:40:53 -0700
Subject: [PATCH] [SVG] Enforce blob URL noopener on SVG <a> navigations

`SVGAElement::DefaultEventHandler` was missing the cross-partition
blob URL noopener enforcement that `HTMLAnchorElementBase` and
`LocalDOMWindow::open` already implement via
`EnforceNoopenerOnBlobURLNavigation`. This allowed an SVG <a>
element in a third-party iframe to open a same-origin blob: URL in
a new window while retaining window.opener, bypassing storage
partitioning.

In this CL we extract the blob URL site-mismatch check into a shared
utility and call it for both HTML <a> and SVG <a> elements.

Bug: 502505715
Change-Id: Ic793f65456768bd93c32fc334164c468ffd8d63a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7875805
Reviewed-by: Kent Tamura <[email protected]>
Reviewed-by: Vinay Singh <[email protected]>
Reviewed-by: Virali Purbey <[email protected]>
Commit-Queue: Divyansh Mangal <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1641494}
---

diff --git a/third_party/blink/renderer/core/html/DEPS b/third_party/blink/renderer/core/html/DEPS
index 35ee14c..c40e6c9 100644
--- a/third_party/blink/renderer/core/html/DEPS
+++ b/third_party/blink/renderer/core/html/DEPS
@@ -14,6 +14,9 @@
     "client_hints_util.h": [
         "+services/network/public/cpp/client_hints.h",
     ],
+    "anchor_element_utils.cc" : [
+        "+base/command_line.h"
+    ],
     "html_anchor_element.cc" : [
         "+base/command_line.h"
     ],
diff --git a/third_party/blink/renderer/core/html/anchor_element_utils.cc b/third_party/blink/renderer/core/html/anchor_element_utils.cc
index ce2c088..db9cea6 100644
--- a/third_party/blink/renderer/core/html/anchor_element_utils.cc
+++ b/third_party/blink/renderer/core/html/anchor_element_utils.cc
@@ -4,6 +4,11 @@
 
 #include "third_party/blink/renderer/core/html/anchor_element_utils.h"
 
+#include "base/command_line.h"
+#include "base/feature_list.h"
+#include "third_party/blink/public/common/features.h"
+#include "third_party/blink/public/common/switches.h"
+#include "third_party/blink/public/mojom/devtools/inspector_issue.mojom-blink.h"
 #include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h"
 #include "third_party/blink/renderer/core/dom/document.h"
 #include "third_party/blink/renderer/core/dom/element.h"
@@ -12,6 +17,7 @@
 #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/settings.h"
+#include "third_party/blink/renderer/core/inspector/inspector_audits_issue.h"
 #include "third_party/blink/renderer/core/loader/frame_load_request.h"
 #include "third_party/blink/renderer/core/loader/navigation_policy.h"
 #include "third_party/blink/renderer/core/loader/ping_loader.h"
@@ -20,7 +26,9 @@
 #include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
 #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
 #include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
+#include "third_party/blink/renderer/platform/network/blink_schemeful_site.h"
 #include "third_party/blink/renderer/platform/weborigin/kurl.h"
+#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
 #include "third_party/blink/renderer/platform/weborigin/security_policy.h"
 #include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
 #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
@@ -35,6 +43,8 @@
 // unresponsive or crashing.
 inline constexpr int kMaxDownloadAttrLength = 1000000;
 
+inline constexpr char kBlobScheme[] = "blob";
+
 // Note: Here it covers download originated from clicking on <a download> link
 // that results in direct download. Features in this method can also be logged
 // from browser for download due to navigations to non-web-renderable content.
@@ -242,4 +252,28 @@
   }
 }
 
+void AnchorElementUtils::EnforceBlobUrlNoopenerIfNeeded(
+    FrameLoadRequest& frame_request,
+    const KURL& url,
+    LocalDOMWindow& window) {
+  if (!url.ProtocolIs(kBlobScheme)) {
+    return;
+  }
+  BlinkSchemefulSite blob_url_site(SecurityOrigin::Create(url));
+  BlinkSchemefulSite top_level_site = window.GetStorageKey().GetTopLevelSite();
+  if (top_level_site != blob_url_site) {
+    if (base::FeatureList::IsEnabled(
+            features::kEnforceNoopenerOnBlobURLNavigation) &&
+        !base::CommandLine::ForCurrentProcess()->HasSwitch(
+            blink::switches::kDisableBlobUrlPartitioning)) {
+      frame_request.SetNoOpener();
+    }
+    UseCounter::Count(window.document(),
+                      WebFeature::kCrossTopLevelSiteBlobURLNavigation);
+    AuditsIssue::ReportPartitioningBlobURLIssue(
+        &window, url.GetString(),
+        mojom::blink::PartitioningBlobURLInfo::kEnforceNoopenerForNavigation);
+  }
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/html/anchor_element_utils.h b/third_party/blink/renderer/core/html/anchor_element_utils.h
index bb896e5..d051e73 100644
--- a/third_party/blink/renderer/core/html/anchor_element_utils.h
+++ b/third_party/blink/renderer/core/html/anchor_element_utils.h
@@ -78,6 +78,13 @@
                                             const AtomicString& referrer_policy,
                                             uint32_t link_relations,
                                             Document& document);
+
+  // Enforces noopener on blob: URL navigations when the blob URL's site
+  // differs from the initiator's top-level site, to prevent storage
+  // partitioning bypasses.
+  static void EnforceBlobUrlNoopenerIfNeeded(FrameLoadRequest& frame_request,
+                                             const KURL& url,
+                                             LocalDOMWindow& window);
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/html/html_anchor_element.cc b/third_party/blink/renderer/core/html/html_anchor_element.cc
index f57541b..c1dd6ea 100644
--- a/third_party/blink/renderer/core/html/html_anchor_element.cc
+++ b/third_party/blink/renderer/core/html/html_anchor_element.cc
@@ -416,25 +416,8 @@
                                          GetExecutionContext(), target,
                                          link_relations_);
 
-  if (completed_url.ProtocolIs("blob")) {
-    auto blob_url_site =
-        BlinkSchemefulSite(SecurityOrigin::Create(completed_url));
-    BlinkSchemefulSite top_level_site =
-        window->GetStorageKey().GetTopLevelSite();
-    if (top_level_site != blob_url_site) {
-      if (base::FeatureList::IsEnabled(
-              features::kEnforceNoopenerOnBlobURLNavigation) &&
-          !base::CommandLine::ForCurrentProcess()->HasSwitch(
-              blink::switches::kDisableBlobUrlPartitioning)) {
-        frame_request.SetNoOpener();
-      }
-      UseCounter::Count(GetDocument(),
-                        WebFeature::kCrossTopLevelSiteBlobURLNavigation);
-      AuditsIssue::ReportPartitioningBlobURLIssue(
-          window, completed_url.GetString(),
-          mojom::blink::PartitioningBlobURLInfo::kEnforceNoopenerForNavigation);
-    }
-  }
+  AnchorElementUtils::EnforceBlobUrlNoopenerIfNeeded(frame_request,
+                                                     completed_url, *window);
 
   frame_request.SetTriggeringEventInfo(
       is_trusted ? mojom::blink::TriggeringEventInfo::kFromTrustedEvent
diff --git a/third_party/blink/renderer/core/svg/svg_a_element.cc b/third_party/blink/renderer/core/svg/svg_a_element.cc
index 2951f24..6ed4ae5 100644
--- a/third_party/blink/renderer/core/svg/svg_a_element.cc
+++ b/third_party/blink/renderer/core/svg/svg_a_element.cc
@@ -208,6 +208,9 @@
           frame_request, frame->GetSettings(), GetExecutionContext(), target,
           link_relations_);
 
+      AnchorElementUtils::EnforceBlobUrlNoopenerIfNeeded(
+          frame_request, resolved_url, *GetDocument().domWindow());
+
       frame_request.SetTriggeringEventInfo(
           event.isTrusted()
               ? mojom::blink::TriggeringEventInfo::kFromTrustedEvent
diff --git a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
index cdd21dd9..f38a8d14 100755
--- a/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
+++ b/third_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.py
@@ -1309,6 +1309,14 @@
         ]
     },
     {
+        'paths': [
+            'third_party/blink/renderer/core/html/anchor_element_utils.cc',
+        ],
+        'allowed': [
+            'base::CommandLine',
+        ]
+    },
+    {
         'paths': ['third_party/blink/renderer/core/frame/dom_window.cc'],
         'allowed': [
             'base::MakeFixedFlatMap',
diff --git a/third_party/blink/web_tests/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html b/third_party/blink/web_tests/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html
index a92e0f7..978370f 100644
--- a/third_party/blink/web_tests/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html
+++ b/third_party/blink/web_tests/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html
@@ -247,5 +247,52 @@
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html b/third_party/blink/web_tests/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html
index a92e0f7..978370f 100644
--- a/third_party/blink/web_tests/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html
+++ b/third_party/blink/web_tests/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html
@@ -247,5 +247,52 @@
   });
 }, "Blob URL area element click should enforce noopener for a cross-top-level-site navigation");
 
+const open_blob_url_window_via_svg_a_click = (blob_url) => `
+  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+  const a = document.createElementNS("http://www.w3.org/2000/svg", "a");
+  a.setAttribute("href", "${blob_url}");
+  a.setAttribute("target", "_blank");
+  a.setAttribute("rel", "opener");
+  const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
+  text.textContent = "click";
+  a.appendChild(text);
+  svg.appendChild(a);
+  document.body.appendChild(svg);
+  a.dispatchEvent(new MouseEvent("click", {bubbles: true}));
+`;
+
+// Tests blob URL SVG `<a target="_blank" rel="opener">` click for same and
+// cross partition iframes.
+promise_test(t => {
+  return new Promise(async (resolve, reject) => {
+    try {
+      // Creates same and cross partition iframes.
+      const noopener_response_queue = token();
+
+      const [cross_site_iframe_uuid, same_site_iframe_uuid] = await create_test_iframes(t, token());
+
+      const blob = new Blob([opener_check_frame_html(noopener_response_queue)], {type : "text/html"});
+      const blob_url = URL.createObjectURL(blob);
+
+      // Attempt to click blob URL in cross partition iframe.
+      await send(cross_site_iframe_uuid, open_blob_url_window_via_svg_a_click(blob_url));
+      const noopener_response_1 = await receive(noopener_response_queue);
+      if (noopener_response_1 !== opener_null_response) {
+        reject(`Blob URL page opener wasn't null in not-same-top-level-site iframe.`);
+      }
+
+      // Attempt to click blob URL in same partition iframe.
+      await send(same_site_iframe_uuid, open_blob_url_window_via_svg_a_click(blob_url));
+      const noopener_response_2 = await receive(noopener_response_queue);
+      if (noopener_response_2 !== opener_not_null_response) {
+        reject(`Blob URL page opener was null in same-top-level-site iframe`);
+      }
+      resolve();
+    } catch (e) {
+      reject(e);
+    }
+  });
+}, "Blob URL SVG <a> element click should enforce noopener for a cross-top-level-site navigation");
+
 </script>
 </body>
diff --git a/third_party/blink/web_tests/virtual/cross-partition-blob-url/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https-expected.txt b/third_party/blink/web_tests/virtual/cross-partition-blob-url/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https-expected.txt
index 0735952..12125a2 100644
--- a/third_party/blink/web_tests/virtual/cross-partition-blob-url/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https-expected.txt
+++ b/third_party/blink/web_tests/virtual/cross-partition-blob-url/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https-expected.txt
@@ -7,5 +7,6 @@
   promise_test: Unhandled rejection with value: "Blob URL page opener wasn't null in not-same-top-level-site iframe."
 [FAIL] Blob URL area element click should enforce noopener for a cross-top-level-site navigation
   promise_test: Unhandled rejection with value: "Blob URL page opener wasn't null in not-same-top-level-site iframe."
+[FAIL] Blob URL SVG <a> element click should enforce noopener for a cross-top-level-site navigation
+  promise_test: Unhandled rejection with value: "Blob URL page opener wasn't null in not-same-top-level-site iframe."
 Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/wpt_internal/partition-blob-url/partition-blob-url-use-counter.https.html b/third_party/blink/web_tests/wpt_internal/partition-blob-url/partition-blob-url-use-counter.https.html
index 2e7f3f70..6425465 100644
--- a/third_party/blink/web_tests/wpt_internal/partition-blob-url/partition-blob-url-use-counter.https.html
+++ b/third_party/blink/web_tests/wpt_internal/partition-blob-url/partition-blob-url-use-counter.https.html
@@ -139,5 +139,52 @@
  });
 }, "kCrossTopLevelSiteBlobURLNavigation UseCounter should be triggered for a Blob URL cross-top-level-site link click.");
 
+const open_blob_url_window_via_svg_a_click = (blob_url, response_queue_name) => `
+  const SVGNS = 'http://www.w3.org/2000/svg';
+  const svg = document.createElementNS(SVGNS, 'svg');
+  const a = document.createElementNS(SVGNS, 'a');
+  a.setAttribute('href', '${blob_url}');
+  a.setAttribute('target', '_blank');
+  a.setAttribute('rel', 'opener');
+  const t = document.createElementNS(SVGNS, 'text');
+  t.textContent = 'click';
+  a.appendChild(t);
+  svg.appendChild(a);
+  document.body.appendChild(svg);
+  a.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+  if (internals.isUseCounted(document, ${kCrossTopLevelSiteBlobURLNavigation}) === false) {
+    return send("${response_queue_name}", "${navigation_counter_not_triggered}");
+  }
+  return send("${response_queue_name}", "${navigation_counter_triggered}");
+`;
+
+// Tests blob URL `<svg:a target="_blank" rel="opener">` click for cross partition iframes.
+promise_test(t => {
+ return new Promise(async (resolve, reject) => {
+   try {
+     clear();
+     // Creates same and cross partition iframes.
+     const response_queue_uuid = token();
+
+     const [cross_site_iframe_uuid, same_site_iframe_uuid] = await create_test_iframes(t, token());
+
+     const blob = new Blob([frame_html], {type : "text/html"});
+     const blob_url = URL.createObjectURL(blob);
+
+     // Attempt to click SVG blob URL link in cross partition iframe.
+     await send(cross_site_iframe_uuid, open_blob_url_window_via_svg_a_click(blob_url, response_queue_uuid));
+     const response = await receive(response_queue_uuid);
+
+     if (response === navigation_counter_not_triggered) {
+       reject(navigation_counter_not_triggered);
+     }
+
+     resolve();
+   } catch (e) {
+     reject(e);
+   }
+ });
+}, "kCrossTopLevelSiteBlobURLNavigation UseCounter should be triggered for a Blob URL cross-top-level-site SVG link click.");
+
 </script>
 </body>
\ No newline at end of file
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.