Skip to content

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

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

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

Conversation

@secengjeff
Copy link

@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

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

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use absolute paths when invoking built-in shell commands

1 participant