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 <[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.
References
On This Page