CVE-2026-11128
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/webshare/navigator_share.cc |
modified | |
TEST_Fthird_party/blink/renderer/modules/webshare/navigator_share_test.cc |
modified |
Files Changed
third_party/blink/renderer/modules/webshare/navigator_share.ccthird_party/blink/renderer/modules/webshare/navigator_share_test.ccthird_party/blink/web_tests/external/wpt/web-share/share-file-url-with-base-tag.https.html
Patch
From 65fe95943efc39034a2e2aeee7c60f1e1de64064 Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Thu, 30 Apr 2026 05:20:29 -0700 Subject: [PATCH] Fix Web Share API URL scheme validation bypass This CL fixes a vulnerability where an HTTPS page could bypass URL scheme restrictions in the Web Share API by setting a <base href> with a 'file:' scheme. The vulnerability was introduced in crrev.com/c/2425977, which added a Chromium-specific exception to the HTTP(S)-only scheme restriction to allow pages to share URLs matching their own scheme. This behavior is not part of the Web Share API specification, which strictly limits shared URLs to HTTP(S) schemes (and potentially other safelisted schemes, though Chromium currently only supports HTTP(S) and the same-origin protocol). We may want to consider removing this non-standard exception in the future to better align with the spec and reduce the attack surface. That CL erroneously used the document's base URL protocol (window.document()->BaseURL().Protocol()) instead of its actual security origin. Since the base URL can be manipulated by the page using a <base> tag, an HTTPS page could set its base URL to 'file:///', causing the renderer to incorrectly permit sharing of 'file:' URLs. The fix involves using the document's actual security origin protocol (GetExecutionContext()->GetSecurityOrigin()->Protocol()) to validate the shared URL's scheme. This protocol represents the document's true origin and is not influenced by the <base> tag. Browser-side secondary validation (defense-in-depth) will be added in a follow-up CL once the approach is finalized. Fixed: 501541341 Bug: 1131755 Change-Id: Ieb56a097cc79ccd13ada9155d1c6aab41e2f7ad1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7800748 Commit-Queue: Andrew Paseltiner <[email protected]> Reviewed-by: Daniel Murphy <[email protected]> Cr-Commit-Position: refs/heads/main@{#1623097} --- diff --git a/third_party/blink/renderer/modules/webshare/navigator_share.cc b/third_party/blink/renderer/modules/webshare/navigator_share.cc index 71d8b47de..85331539 100644 --- a/third_party/blink/renderer/modules/webshare/navigator_share.cc +++ b/third_party/blink/renderer/modules/webshare/navigator_share.cc @@ -92,9 +92,11 @@ if (data.hasUrl()) { url = window.CompleteURL(data.url()); - if (!url.IsValid() || - (!url.ProtocolIsInHttpFamily() && - url.Protocol() != window.document()->BaseURL().Protocol())) { + if (!url.IsValid() || (!url.ProtocolIsInHttpFamily() && + url.Protocol() != window.document() + ->GetExecutionContext() + ->GetSecurityOrigin() + ->Protocol())) { if (exception_state) { exception_state->ThrowTypeError("Invalid URL"); } diff --git a/third_party/blink/renderer/modules/webshare/navigator_share_test.cc b/third_party/blink/renderer/modules/webshare/navigator_share_test.cc index 5ce395a..dadcd37 100644 --- a/third_party/blink/renderer/modules/webshare/navigator_share_test.cc +++ b/third_party/blink/renderer/modules/webshare/navigator_share_test.cc @@ -236,4 +236,24 @@ WebFeature::kWebShareUnsuccessfulContainingFiles)); } +TEST_F(NavigatorShareTest, ShareFileUrlWithBaseTag) { + GetDocument().SetBaseURLOverride(KURL("file:///")); + + const String url = "file:///etc/passwd"; + ShareData* share_data = MakeGarbageCollected<ShareData>(); + share_data->setUrl(url); + + LocalFrame::NotifyUserActivation( + &GetFrame(), mojom::UserActivationNotificationType::kTest); + Navigator* navigator = GetFrame().DomWindow()->navigator(); + DummyExceptionStateForTesting exception_state; + NavigatorShare::share(GetScriptState(), *navigator, share_data, + exception_state); + + // Regression test for crbug.com/501541341. + // Verify that the URL is rejected by CanShareInternal even when the + // document's base URL protocol is manipulated to match the shared URL. + EXPECT_TRUE(exception_state.HadException()); +} + } // namespace blink diff --git a/third_party/blink/web_tests/external/wpt/web-share/share-file-url-with-base-tag.https.html b/third_party/blink/web_tests/external/wpt/web-share/share-file-url-with-base-tag.https.html new file mode 100644 index 0000000..143b43c6 --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/web-share/share-file-url-with-base-tag.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <title>WebShare Test: Share with a file URL and a base tag</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + </head> + <body> + <script> + // Dynamically add a <base> tag after scripts have loaded. + const base = document.createElement('base'); + base.href = 'file:///'; + document.head.appendChild(base); + + test(() => { + assert_false( + navigator.canShare({ url: "file:///etc/passwd" }), + "file URL should not be allowed even with a file: base URL" + ); + }, "canShare() rejects file:// URLs even with a file: base URL"); + + promise_test(async t => { + await test_driver.bless(); + const promise = navigator.share({ url: "file:///etc/passwd" }); + return promise_rejects_js(t, TypeError, promise); + }, "share() rejects file:// URLs even with a file: base URL"); + + promise_test(async t => { + await test_driver.bless(); + // A relative URL "passwd" resolved against "file:///" becomes "file:///passwd". + // This should also be rejected. + const promise = navigator.share({ url: "passwd" }); + return promise_rejects_js(t, TypeError, promise); + }, "share() rejects relative URLs that resolve to file:// scheme"); + </script> + </body> +</html>
Regression Test / PoC
diff --git a/third_party/blink/renderer/modules/webshare/navigator_share_test.cc b/third_party/blink/renderer/modules/webshare/navigator_share_test.cc
index 5ce395a..dadcd37 100644
--- a/third_party/blink/renderer/modules/webshare/navigator_share_test.cc
+++ b/third_party/blink/renderer/modules/webshare/navigator_share_test.cc
@@ -236,4 +236,24 @@
WebFeature::kWebShareUnsuccessfulContainingFiles));
}
+TEST_F(NavigatorShareTest, ShareFileUrlWithBaseTag) {
+ GetDocument().SetBaseURLOverride(KURL("file:///"));
+
+ const String url = "file:///etc/passwd";
+ ShareData* share_data = MakeGarbageCollected<ShareData>();
+ share_data->setUrl(url);
+
+ LocalFrame::NotifyUserActivation(
+ &GetFrame(), mojom::UserActivationNotificationType::kTest);
+ Navigator* navigator = GetFrame().DomWindow()->navigator();
+ DummyExceptionStateForTesting exception_state;
+ NavigatorShare::share(GetScriptState(), *navigator, share_data,
+ exception_state);
+
+ // Regression test for crbug.com/501541341.
+ // Verify that the URL is rejected by CanShareInternal even when the
+ // document's base URL protocol is manipulated to match the shared URL.
+ EXPECT_TRUE(exception_state.HadException());
+}
+
} // namespace blink
diff --git a/third_party/blink/web_tests/external/wpt/web-share/share-file-url-with-base-tag.https.html b/third_party/blink/web_tests/external/wpt/web-share/share-file-url-with-base-tag.https.html
new file mode 100644
index 0000000..143b43c6
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/web-share/share-file-url-with-base-tag.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>WebShare Test: Share with a file URL and a base tag</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ </head>
+ <body>
+ <script>
+ // Dynamically add a <base> tag after scripts have loaded.
+ const base = document.createElement('base');
+ base.href = 'file:///';
+ document.head.appendChild(base);
+
+ test(() => {
+ assert_false(
+ navigator.canShare({ url: "file:///etc/passwd" }),
+ "file URL should not be allowed even with a file: base URL"
+ );
+ }, "canShare() rejects file:// URLs even with a file: base URL");
+
+ promise_test(async t => {
+ await test_driver.bless();
+ const promise = navigator.share({ url: "file:///etc/passwd" });
+ return promise_rejects_js(t, TypeError, promise);
+ }, "share() rejects file:// URLs even with a file: base URL");
+
+ promise_test(async t => {
+ await test_driver.bless();
+ // A relative URL "passwd" resolved against "file:///" becomes "file:///passwd".
+ // This should also be rejected.
+ const promise = navigator.share({ url: "passwd" });
+ return promise_rejects_js(t, TypeError, promise);
+ }, "share() rejects relative URLs that resolve to file:// scheme");
+ </script>
+ </body>
+</html>
Original Bug Report
navigator.share() scheme bypass via <base href> leading to potential local file exfiltration
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.
Overview: A logic flaw in the Web Share API allows an HTTPS page to bypass URL scheme restrictions by setting the document’s <base href> to a file:/// URL. The browser process subsequently fails to validate the shared URL’s scheme before passing it to the operating system. On macOS, this can result in the native share sheet treating the URL as a local file, potentially leading to file exfiltration if the user interacts with the share dialog.
Affected files:
third_party/blink/renderer/modules/webshare/navigator_share.ccchrome/browser/webshare/share_service_impl.ccthird_party/blink/renderer/core/dom/document.cccontent/app_shim_remote_cocoa/render_widget_host_ns_view_bridge.mmchrome/browser/webshare/mac/sharing_service_operation.mmchrome/browser/webshare/win/share_operation.cc
Estimated timestamp from git blame: 2026-03-03
Description
A vulnerability exists in the Web Share API where the renderer-side validation of the shared URL’s scheme can be bypassed using a <base href> element. This is compounded by a lack of independent scheme validation in the browser process.
In third_party/blink/renderer/modules/webshare/navigator_share.cc, the CanShareInternal() function validates the scheme of the URL provided to navigator.share(). It explicitly allows non-HTTP(S) schemes if the URL’s protocol matches the document’s base URL protocol: url.Protocol() == window.document()->BaseURL().Protocol().
The document’s base URL is derived from the <base href> element. In third_party/blink/renderer/core/dom/document.cc, Document::ProcessBaseElement() blocks data: and javascript: schemes but does not explicitly block file: schemes from being set as the base URL. Therefore, an HTTPS page can include <base href="file:///">, which sets the document’s BaseURL().Protocol() to "file". Consequently, a call to navigator.share({url: 'file:///etc/passwd'}) will successfully bypass the renderer-side check.
When the renderer sends the share request via the blink.mojom.ShareService::Share IPC, the browser-side implementation in chrome/browser/webshare/share_service_impl.cc fails to validate the share_url’s scheme against the process’s security policy. It does not invoke RenderProcessHostImpl::FilterURL or check ChildProcessSecurityPolicy to ensure the renderer is authorized to request file: URLs. The URL is passed verbatim to the platform-specific share handler.
On macOS 13+, RenderWidgetHostNSViewBridge::ShowSharingServicePicker converts the GURL to a file: scheme NSURL. The macOS NSSharingServicePicker treats file: scheme NSURL items as local file references. If a user is tricked into sharing the item (e.g., via Mail or AirDrop), the operating system will read the local file contents (e.g., /etc/passwd) and attach it to the shared content. The attacker can control the title field to mask the actual URL in the share preview.
Note: These are potential steps and impacts, based on static code analysis.
Suggested Reproduction Steps
- Host a webpage over HTTPS containing the following content:
<!doctype html> <base href="file:///"> <button id="shareBtn">Click to Share</button> <script> shareBtn.onclick = () => { navigator.share({ title: 'Important Document', url: 'file:///etc/passwd' }); }; </script> - On macOS 13+, click the button to trigger
navigator.share()(requires transient user activation). - When the macOS system share picker appears, select a target like ‘Mail’.
- Observe that the contents of
/etc/passwdare attached to the resulting email draft.
Suggested Fix
- Renderer Check: Update
CanShareInternal()innavigator_share.ccto validate the shared URL’s scheme against the document’s Security Origin scheme (or explicitly require HTTP/HTTPS), rather than relying on the mutable Base URL protocol. - Browser Validation: Introduce robust scheme validation in the browser process (
ShareServiceImpl). Theshare_urlshould be validated against theChildProcessSecurityPolicyto ensure the originating renderer process is authorized to access the provided scheme (preventing sandboxed renderers from passingfile:,chrome:, or other privileged URLs).
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.