Skip to content

gh-137586: Replace 'osascript' with 'open' on macOS in webbrowser#146439

Open
secengjeff wants to merge 16 commits intopython:mainfrom
secengjeff:gh-137586-macosx-open
Open

gh-137586: Replace 'osascript' with 'open' on macOS in webbrowser#146439
secengjeff wants to merge 16 commits intopython:mainfrom
secengjeff:gh-137586-macosx-open

Conversation

@secengjeff
Copy link
Copy Markdown

@secengjeff secengjeff commented Mar 26, 2026

Replaces MacOSXOSAScript, which pipes AppleScript to osascript, with a new MacOSX class that calls /usr/bin/open directly via subprocess.run. MacOSXOSAScript is deprecated with a DeprecationWarning pointing users to MacOSX.

Why

osascript is a general-purpose AppleScript interpreter and a known LOObin (Living Off the Land binary). Because it can execute arbitrary code, it is routinely blocked by endpoint security tooling on managed Macs. This causes webbrowser.open() to break for users of any Python application that depends on the webbrowser library, with no obvious connection to osascript as the cause.

/usr/bin/open is Apple's purpose-built URL-opening primitive. It passes the URL directly to the OS-registered URL handler with no scripting interpreter involved, and is not subject to the same endpoint security restrictions.

Security

This fixes gh-137586. The existing os.popen("osascript", "w") call resolves via PATH, creating a PATH-injection vector. The open PR for that issue (#137584) proposes switching to /usr/bin/osascript; this change eliminates the dependency entirely.

File injection safety

/usr/bin/open <url> dispatches via the OS file handler, which means a file:// URL pointing to an .app bundle or installer would launch it rather than open it in a browser. To prevent this, non-http(s) URLs are routed through the browser explicitly using /usr/bin/open -b <bundle-id>, ensuring the URL is always handled by a browser regardless of scheme.

Named browsers use a static bundle ID map for common browsers (com.google.Chrome, org.mozilla.firefox, com.apple.Safari, etc.). Unknown named browsers fall back to -a. For the default browser, the bundle ID is resolved at runtime via the Objective-C runtime using the same NSWorkspace.URLForApplicationToOpenURL lookup that MacOSXOSAScript performed via AppleScript, with a graceful fallback to direct open if ctypes is unavailable.

Named browser support

MacOSXOSAScript used tell application "<name>" to target specific browsers. MacOSX preserves this with /usr/bin/open -b <bundle-id> for known browsers and /usr/bin/open -a <name> for others.

Testing

Tested locally on macOS with the default browser and named browsers (Safari, Chrome). Unit tests added covering: default http/https open, non-http URL bundle ID routing, bundle ID lookup fallback, named browser with known bundle ID, named browser fallback to -a, failure case, and the deprecation warning.

Changes

  • Lib/webbrowser.py: add _macos_default_browser_bundle_id(), add MacOSX with bundle ID map, deprecate MacOSXOSAScript, update register_standard_browsers()
  • Lib/test/test_webbrowser.py: add MacOSXTest and MacOSXOSAScriptDeprecationTest
  • Doc/library/webbrowser.rst: add .. deprecated:: 3.14 entry for MacOSXOSAScript

Fixes gh-137586.

…ate MacOSXOSAScript

Add a new MacOSX class that opens URLs via subprocess.run(['/usr/bin/open', ...])
instead of piping AppleScript to osascript. For named browsers, /usr/bin/open -a
<name> is used; for the default browser, /usr/bin/open <url> defers directly to
the OS URL handler.

MacOSXOSAScript is deprecated with a DeprecationWarning pointing users to MacOSX.
register_standard_browsers() is updated to use MacOSX for all macOS registrations.

osascript is a general-purpose scripting interpreter that is routinely blocked on
managed endpoints due to its abuse potential, causing webbrowser.open() to fail
silently. /usr/bin/open is Apple's purpose-built URL-opening primitive and carries
no such restrictions. This also eliminates the PATH-injection vector in the existing
os.popen("osascript", "w") call.
…pt deprecation

Add MacOSXTest covering default browser open, named browser open, and failure
case (non-zero returncode). Add MacOSXOSAScriptDeprecationTest verifying that
instantiating MacOSXOSAScript emits a DeprecationWarning. All tests mock
subprocess.run.
@bedevere-app
Copy link
Copy Markdown

bedevere-app bot commented Mar 26, 2026

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@python-cla-bot
Copy link
Copy Markdown

python-cla-bot bot commented Mar 26, 2026

All commit authors signed the Contributor License Agreement.

CLA signed

@secengjeff secengjeff changed the title gh-137586: Replace MacOSXOSAScript with MacOSX on macOS, using /usr/bin/open gh-137586: Replace 'osascript' with 'open' on macOS Mar 26, 2026
- Add test_default to MacOSXTest asserting webbrowser.get() returns MacOSX
- Remove test_default from MacOSXOSAScriptTest (no longer the registered default)
- Suppress DeprecationWarning in MacOSXOSAScriptTest setUp and test_explicit_browser
  using warnings.catch_warnings() so tests for OSAScript behaviour still run cleanly
- Add warnings import
…ia OS handler

For non-http(s) URLs (e.g. file://), /usr/bin/open dispatches via the OS
file handler, which would launch an .app bundle rather than open it in a
browser. Fix this by routing non-http(s) URLs through the browser explicitly
using /usr/bin/open -b <bundle-id>.

Named browsers use a static bundle ID map (Chrome, Firefox, Safari, Chromium,
Opera, Edge). Unknown named browsers fall back to -a. For the default browser,
the bundle ID is resolved at runtime via the Objective-C runtime using
NSWorkspace.URLForApplicationToOpenURL, the same lookup MacOSXOSAScript
performed via AppleScript. Falls back to direct open if ctypes is unavailable.

http/https URLs with the default browser continue to use /usr/bin/open
directly, as macOS always routes these to the registered browser.
…ult_browser_bundle_id

NSWorkspace is an AppKit class and is not registered in the ObjC runtime
until AppKit is loaded. Without the explicit LoadLibrary call, objc_getClass
returns nil for NSWorkspace, causing the entire lookup to silently fall back
to /usr/bin/open without -b.
@vstinner vstinner changed the title gh-137586: Replace 'osascript' with 'open' on macOS gh-137586: Replace 'osascript' with 'open' on macOS in webbrowser Mar 27, 2026
(4)
Only on iOS.

.. deprecated:: 3.14
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.15

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. deprecated:: 3.14
.. deprecated:: next

And precede this deprecated block with a versionadded for the new class, and move both after versionchanged:: 3.13 below.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the MacOSXOSAScripts in the table above need updating, and should we also add chrome and firefox (with note (3)) as also returning the new class?

@gpshead gpshead requested a review from ned-deily April 5, 2026 22:51
@ned-deily ned-deily requested review from a team and ronaldoussoren April 6, 2026 00:19
Copy link
Copy Markdown
Contributor

@ronaldoussoren ronaldoussoren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this PR.

The direct security issue can be fixed by changing the invocation of osascript to /usr/bin/osascript. Blocking osascript while allowing usage of Python is IMHO security theatre.

On modernist systems (macOS 10.15 or later) it is probably possible to just use NSWorkspace directly: it has all the moving peaces to implement what we need for webbrowser.open although I haven't thought through the implications of doing this yet. In particular, -[NSWorkpace openURLs:withApplicationAtURL:configuration:completionHandler:] is asynchronous which makes it harder to report errors.

#

if sys.platform == 'darwin':
def _macos_default_browser_bundle_id():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function leaks memory due to not performing objective-c reference count updates.

Also: This introduces a new dependency on an Apple system framework, which in the past has caused problems for folks starting new worker processes using os.fork (without exec-in a new executable).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that hardcoding /usr/bin/osascript addresses the PATH injection issue directly. This PR is intended as an incremental improvement on top of that.

The core motivation is that osascript is increasingly blocked at the EDR/MDM level in enterprise environments as a broad response to ClickFix campaigns and supply chain attacks like the axios incident. /usr/bin/open is both incrementally safer as a purpose-built URL dispatch primitive and far less likely to be subject to those same restrictions. When osascript is blocked, users of Python-based tools may see a failure with no obvious connection to osascript, and the path to diagnosing an endpoint security policy conflict is not straightforward for most users.

On _macos_default_browser_bundle_id(): agreed, I'll fix the missing Objective-C reference count cleanup and address the os.fork() concern.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ended up replacing the AppKit/ctypes implementation with a plistlib read of the LaunchServices preferences file to address the memory and forking risks.

@bedevere-app
Copy link
Copy Markdown

bedevere-app bot commented Apr 6, 2026

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

(4)
Only on iOS.

.. deprecated:: 3.14
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. deprecated:: 3.14
.. deprecated:: next

And precede this deprecated block with a versionadded for the new class, and move both after versionchanged:: 3.13 below.

(4)
Only on iOS.

.. deprecated:: 3.14
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the MacOSXOSAScripts in the table above need updating, and should we also add chrome and firefox (with note (3)) as also returning the new class?

'firefox': 'org.mozilla.firefox',
'safari': 'com.apple.Safari',
'chromium': 'org.chromium.Chromium',
'opera': 'com.operasoftware.Opera',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have Opera installed on macOS, and confirm this is the correct bundle ID in the plist, but whilst this works:

import webbrowser
web = webbrowser.get("firefox")
web.open("https://www.python.org/")

This doesn't:

import webbrowser
web = webbrowser.get("opera")
web.open("https://www.python.org/")
Traceback (most recent call last):
  File "/Users/hugo/github/python/cpython/main/1.py", line 2, in <module>
    web = webbrowser.get("opera")
  File "/Users/hugo/github/python/cpython/main/Lib/webbrowser.py", line 68, in get
    raise Error("could not locate runnable browser")
webbrowser.Error: could not locate runnable browser

Do these new ones need registering too, or removing from here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for catching this. I registered the browsers and fixed the Edge bundle. Tested both Opera and Edge locally.

@merwok merwok added type-feature A feature request or enhancement OS-mac labels Apr 6, 2026
secengjeff and others added 4 commits April 6, 2026 09:22
…_standard_browsers on macOS

These browsers were present in MacOSX._BUNDLE_IDS but not registered,
causing webbrowser.get("opera") etc. to raise Error: could not locate
runnable browser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@secengjeff
Copy link
Copy Markdown
Author

I have made the requested changes; please review again

@bedevere-app
Copy link
Copy Markdown

bedevere-app bot commented Apr 6, 2026

Thanks for making the requested changes!

@ronaldoussoren: please review the changes made to this pull request.

@bedevere-app bedevere-app bot requested a review from ronaldoussoren April 6, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting change review OS-mac type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use absolute paths when invoking built-in shell commands

5 participants