Chrome · SVG
CVE-2026-17911
Logic Error in SVG
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/html/anchor_element_utils.cc |
modified | |
ifthird_party/blink/renderer/core/html/html_anchor_element.cc |
modified |
Files Changed
third_party/blink/renderer/core/html/DEPSthird_party/blink/renderer/core/html/anchor_element_utils.ccthird_party/blink/renderer/core/html/anchor_element_utils.hthird_party/blink/renderer/core/html/html_anchor_element.ccthird_party/blink/renderer/core/svg/svg_a_element.ccthird_party/blink/tools/blinkpy/presubmit/audit_non_blink_usage.pythird_party/blink/web_tests/external/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html
Patch
From 17a3040239ed7e02e18ba84157520c9ba052b8ae Mon Sep 17 00:00:00 2001
From: Divyansh Mangal <dmangal@microsoft.com>
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 <tkent@chromium.org>
Reviewed-by: Vinay Singh <vinaysingh@microsoft.com>
Reviewed-by: Virali Purbey <viralipurbey@microsoft.com>
Commit-Queue: Divyansh Mangal <dmangal@microsoft.com>
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.
References
On This Page