Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Web Share
DescriptionInsufficient validation of untrusted input in Web Share
ComponentWeb Share
Bug ClassLogic Error
Tracker501541341
Fix commit65fe95943efc (chromium/src) +65/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/webshare/navigator_share.cc
modified
TEST_F
third_party/blink/renderer/modules/webshare/navigator_share_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/webshare/navigator_share.cc
  • third_party/blink/renderer/modules/webshare/navigator_share_test.cc
  • third_party/blink/web_tests/external/wpt/web-share/share-file-url-with-base-tag.https.html
From 65fe95943efc39034a2e2aeee7c60f1e1de64064 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
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 <apaseltiner@chromium.org>
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
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>
Loading diff…

Regression Test / PoC

shipped with the fix
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>
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • chrome/browser/webshare/share_service_impl.cc
  • third_party/blink/renderer/core/dom/document.cc
  • content/app_shim_remote_cocoa/render_widget_host_ns_view_bridge.mm
  • chrome/browser/webshare/mac/sharing_service_operation.mm
  • chrome/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

  1. 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>
    
  2. On macOS 13+, click the button to trigger navigator.share() (requires transient user activation).
  3. When the macOS system share picker appears, select a target like ‘Mail’.
  4. Observe that the contents of /etc/passwd are attached to the resulting email draft.

Suggested Fix

  1. Renderer Check: Update CanShareInternal() in navigator_share.cc to 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.
  2. Browser Validation: Introduce robust scheme validation in the browser process (ShareServiceImpl). The share_url should be validated against the ChildProcessSecurityPolicy to ensure the originating renderer process is authorized to access the provided scheme (preventing sandboxed renderers from passing file:, 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.

View on issue tracker