Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions Lib/test/test_webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,9 @@ def test_default_http_open(self):
)
self.assertTrue(result)

def test_default_non_http_uses_bundle_id(self):
# Non-http(s) URLs (e.g. file://) must be routed through the browser
# via -b <bundle-id> to prevent OS file handler dispatch.
def test_default_file_uses_bundle_id(self):
# file:// URLs must be routed through the browser via -b <bundle-id>
# to prevent OS file handler dispatch.
file_url = 'file:///tmp/test.html'
browser = webbrowser.MacOS('default')
with mock.patch('webbrowser._macos_default_browser_bundle_id',
Expand All @@ -377,6 +377,23 @@ def test_default_non_http_uses_bundle_id(self):
)
self.assertTrue(result)

def test_default_custom_scheme_uses_plain_open(self):
# Custom app URI schemes (e.g. vscode://, slack://) are not routed
# through the default browser's bundle ID -- the OS should resolve
# the scheme's own registered handler (gh-149454).
custom_url = 'vscode://vscode-remote/ssh-remote+host/workspace'
browser = webbrowser.MacOS('default')
with mock.patch('webbrowser._macos_default_browser_bundle_id') as mock_bundle, \
mock.patch('subprocess.run') as mock_run:
mock_run.return_value = mock.Mock(returncode=0)
result = browser.open(custom_url)
mock_run.assert_called_once_with(
['/usr/bin/open', custom_url],
stderr=subprocess.DEVNULL,
)
mock_bundle.assert_not_called()
self.assertTrue(result)

def test_named_known_browser_uses_bundle_id(self):
# Named browsers with a known bundle ID use /usr/bin/open -b.
browser = webbrowser.MacOS('safari')
Expand Down
27 changes: 21 additions & 6 deletions Lib/webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,11 +651,16 @@ class MacOS(BaseBrowser):
For http/https URLs with the default browser, /usr/bin/open is called
directly; macOS routes these to the registered browser.

For all other URL schemes (e.g. file://) and for named browsers,
/usr/bin/open -b <bundle-id> is used so that the URL is always passed
to a browser application rather than dispatched by the OS file handler.
This prevents file injection attacks where a file:// URL pointing to an
executable bundle could otherwise be launched by the OS.
For file:// URLs and for named browsers, /usr/bin/open -b <bundle-id>
is used so that the URL is always passed to a browser application
rather than dispatched by the OS file handler. This prevents file
injection attacks where a file:// URL pointing to an executable
bundle could otherwise be launched by the OS.

Other URL schemes (e.g. custom app schemes like vscode:// or
slack://) are passed to /usr/bin/open without a bundle ID, so macOS
resolves the scheme's own registered handler application instead of
forcing it through the default browser.

Named browsers with known bundle IDs use -b; unknown names fall back
to -a.
Expand All @@ -678,9 +683,19 @@ def open(self, url, new=0, autoraise=True):
proto, sep, _ = url.partition(':')
if sep and proto.lower() in {'http', 'https'}:
cmd = ['/usr/bin/open', url]
else:
elif sep and proto.lower() == 'file':
# file:// URLs must be routed through the default
# browser explicitly: the OS's generic file handler
# dispatches file: URLs by the target file's type (e.g.
# a text editor for .html), not to a web browser.
bundle_id = _macos_default_browser_bundle_id()
cmd = ['/usr/bin/open', '-b', bundle_id, url]
else:
# Other custom URI schemes (e.g. vscode://, slack://)
# aren't web content: let the OS resolve the scheme's
# own registered handler instead of forcing it through
# the browser (gh-149454).
cmd = ['/usr/bin/open', url]
else:
bundle_id = self._BUNDLE_IDS.get(self.name.lower())
if bundle_id:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
On macOS, :func:`webbrowser.open` no longer forces custom (non-``file``,
non-``http``/``https``) URI schemes such as ``vscode://`` or ``slack://``
through the default web browser. It now lets the OS resolve the scheme's
own registered handler application, restoring the behavior from Python
3.13 and earlier. This undoes an unintended side effect of the fix in
gh-130535, which addressed :func:`webbrowser.open` mishandling ``file://``
URLs.
Loading